From 5594d89cbd9216f41b88c5a3b3ccce2f03732c05 Mon Sep 17 00:00:00 2001
From: Spooks <62370103+Spooks4576@users.noreply.github.com>
Date: Wed, 15 Jul 2026 13:27:47 -0600
Subject: [PATCH] Rewrite Vulkan backend with native renderer
---
.../workflows/windows-native-backend-test.yml | 93 ++
.github/workflows/workflow.yml | 26 +
src/SharpEmu.GUI/GuiSettings.cs | 3 +
src/SharpEmu.GUI/Languages/en.json | 5 +
src/SharpEmu.GUI/MainWindow.axaml | 13 +
src/SharpEmu.GUI/MainWindow.axaml.cs | 23 +
src/SharpEmu.Gpu.Vulkan.Native/CMakeLists.txt | 85 ++
.../include/sharpemu_gpu_vulkan.h | 105 ++
.../src/backend.cpp | 1329 +++++++++++++++++
.../tests/abi_test.cpp | 55 +
.../tests/exports_test.cpp | 14 +
.../tests/smoke.comp | 3 +
.../tests/smoke.frag | 4 +
.../tests/smoke.vert | 3 +
src/SharpEmu.Libs/Gpu/GuestGpu.cs | 9 +-
.../Gpu/NativeVulkan/NativeGpuInputSource.cs | 71 +
.../Gpu/NativeVulkan/NativeGpuPacket.cs | 182 +++
.../Gpu/NativeVulkan/NativeVulkanApi.cs | 172 +++
.../NativeVulkanGuestGpuBackend.cs | 256 ++++
src/SharpEmu.Libs/SharpEmu.Libs.csproj | 38 +
.../SpirvFixedShaders.cs | 30 +
21 files changed, 2518 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/windows-native-backend-test.yml
create mode 100644 src/SharpEmu.Gpu.Vulkan.Native/CMakeLists.txt
create mode 100644 src/SharpEmu.Gpu.Vulkan.Native/include/sharpemu_gpu_vulkan.h
create mode 100644 src/SharpEmu.Gpu.Vulkan.Native/src/backend.cpp
create mode 100644 src/SharpEmu.Gpu.Vulkan.Native/tests/abi_test.cpp
create mode 100644 src/SharpEmu.Gpu.Vulkan.Native/tests/exports_test.cpp
create mode 100644 src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.comp
create mode 100644 src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.frag
create mode 100644 src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.vert
create mode 100644 src/SharpEmu.Libs/Gpu/NativeVulkan/NativeGpuInputSource.cs
create mode 100644 src/SharpEmu.Libs/Gpu/NativeVulkan/NativeGpuPacket.cs
create mode 100644 src/SharpEmu.Libs/Gpu/NativeVulkan/NativeVulkanApi.cs
create mode 100644 src/SharpEmu.Libs/Gpu/NativeVulkan/NativeVulkanGuestGpuBackend.cs
diff --git a/.github/workflows/windows-native-backend-test.yml b/.github/workflows/windows-native-backend-test.yml
new file mode 100644
index 0000000..d6c0f65
--- /dev/null
+++ b/.github/workflows/windows-native-backend-test.yml
@@ -0,0 +1,93 @@
+# Copyright (C) 2026 SharpEmu Emulator Project
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+name: Test Windows Native Vulkan Backend
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ test-windows-native-vulkan:
+ name: Build and test win-x64 native backend
+ runs-on: windows-latest
+ env:
+ DOTNET_NOLOGO: true
+ NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
+ NATIVE_BUILD_DIR: ${{ github.workspace }}\artifacts\native\gpu-vulkan
+ PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64-native-test
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.103
+ cache: true
+ cache-dependency-path: |
+ Directory.Packages.props
+ src/**/packages.lock.json
+
+ - name: Install native dependencies
+ shell: pwsh
+ run: |
+ & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install sdl3:x64-windows vulkan:x64-windows
+
+ - name: Configure native backend
+ shell: pwsh
+ run: |
+ cmake `
+ -S src/SharpEmu.Gpu.Vulkan.Native `
+ -B "$env:NATIVE_BUILD_DIR" `
+ -A x64 `
+ -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" `
+ -DVCPKG_TARGET_TRIPLET=x64-windows `
+ -DBUILD_TESTING=ON
+
+ - name: Build native backend and Windows-safe ABI test
+ run: cmake --build "$env:NATIVE_BUILD_DIR" --config Release --target sharpemu_gpu_vulkan_exports_test
+
+ - name: Run native DLL load and ABI test
+ run: ctest --test-dir "$env:NATIVE_BUILD_DIR" -C Release -R sharpemu_gpu_vulkan_exports_test --output-on-failure
+
+ - name: Restore solution
+ run: dotnet restore SharpEmu.slnx --locked-mode
+
+ - name: Build solution
+ run: dotnet build SharpEmu.slnx -c Release --no-restore
+
+ - name: Publish win-x64 emulator
+ run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
+
+ - name: Verify published native runtime
+ shell: pwsh
+ run: |
+ $required = @(
+ (Join-Path $env:PUBLISH_DIR 'SharpEmu.exe'),
+ (Join-Path $env:PUBLISH_DIR 'sharpemu_gpu_vulkan.dll'),
+ (Join-Path $env:PUBLISH_DIR 'SDL3.dll')
+ )
+ foreach ($path in $required) {
+ if (-not (Test-Path $path)) {
+ throw "Missing published runtime file: $path"
+ }
+ }
+
+ $exports = & dumpbin.exe /nologo /exports (Join-Path $env:PUBLISH_DIR 'sharpemu_gpu_vulkan.dll')
+ foreach ($name in @('se_gpu_abi_version', 'se_gpu_create', 'se_gpu_submit_draw', 'se_gpu_present_bgra')) {
+ if ($exports -notmatch "\b$name\b") {
+ throw "Missing native GPU export: $name"
+ }
+ }
+
+ - name: Upload tested Windows package
+ uses: actions/upload-artifact@v7
+ with:
+ name: sharpemu-win-x64-native-backend-test-${{ github.sha }}
+ path: ${{ env.PUBLISH_DIR }}
+ if-no-files-found: error
+ retention-days: 7
diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
index 90ded6c..c4c379d 100644
--- a/.github/workflows/workflow.yml
+++ b/.github/workflows/workflow.yml
@@ -96,6 +96,19 @@ jobs:
Directory.Packages.props
Directory.Build.props
+ - name: Install Windows native GPU dependencies
+ shell: pwsh
+ run: |
+ & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install sdl3:x64-windows vulkan:x64-windows
+
+ cmake `
+ -S src/SharpEmu.Gpu.Vulkan.Native `
+ -B artifacts/native/gpu-vulkan `
+ -A x64 `
+ -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" `
+ -DVCPKG_TARGET_TRIPLET=x64-windows `
+ -DBUILD_TESTING=OFF
+
- name: Restore solution
run: dotnet restore SharpEmu.slnx
@@ -114,6 +127,19 @@ jobs:
- name: Publish win-x64 CLI
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
+ - name: Verify Windows native GPU runtime
+ shell: pwsh
+ run: |
+ $required = @(
+ (Join-Path $env:PUBLISH_DIR 'sharpemu_gpu_vulkan.dll'),
+ (Join-Path $env:PUBLISH_DIR 'SDL3.dll')
+ )
+ foreach ($path in $required) {
+ if (-not (Test-Path $path)) {
+ throw "Missing native GPU runtime: $path"
+ }
+ }
+
- name: Create release archive
run: |
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
diff --git a/src/SharpEmu.GUI/GuiSettings.cs b/src/SharpEmu.GUI/GuiSettings.cs
index d07a494..76fb6ef 100644
--- a/src/SharpEmu.GUI/GuiSettings.cs
+++ b/src/SharpEmu.GUI/GuiSettings.cs
@@ -23,6 +23,9 @@ public sealed class GuiSettings
public bool StrictDynlibResolution { get; set; }
+ /// GPU implementation selected for newly launched games.
+ public string RenderingBackend { get; set; } = "Legacy";
+
///
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if is null.
///
diff --git a/src/SharpEmu.GUI/Languages/en.json b/src/SharpEmu.GUI/Languages/en.json
index cbc1876..750c8a6 100644
--- a/src/SharpEmu.GUI/Languages/en.json
+++ b/src/SharpEmu.GUI/Languages/en.json
@@ -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.",
diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml
index d818c07..9fa70d1 100644
--- a/src/SharpEmu.GUI/MainWindow.axaml
+++ b/src/SharpEmu.GUI/MainWindow.axaml
@@ -203,6 +203,19 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpEmu.GUI/MainWindow.axaml.cs b/src/SharpEmu.GUI/MainWindow.axaml.cs
index 0fbc735..ee73c47 100644
--- a/src/SharpEmu.GUI/MainWindow.axaml.cs
+++ b/src/SharpEmu.GUI/MainWindow.axaml.cs
@@ -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));
diff --git a/src/SharpEmu.Gpu.Vulkan.Native/CMakeLists.txt b/src/SharpEmu.Gpu.Vulkan.Native/CMakeLists.txt
new file mode 100644
index 0000000..07d726b
--- /dev/null
+++ b/src/SharpEmu.Gpu.Vulkan.Native/CMakeLists.txt
@@ -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
+ $
+ $
+ 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()
diff --git a/src/SharpEmu.Gpu.Vulkan.Native/include/sharpemu_gpu_vulkan.h b/src/SharpEmu.Gpu.Vulkan.Native/include/sharpemu_gpu_vulkan.h
new file mode 100644
index 0000000..df47c56
--- /dev/null
+++ b/src/SharpEmu.Gpu.Vulkan.Native/include/sharpemu_gpu_vulkan.h
@@ -0,0 +1,105 @@
+/* Copyright (C) 2026 SharpEmu Emulator Project
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+#include
+#include
+
+#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
diff --git a/src/SharpEmu.Gpu.Vulkan.Native/src/backend.cpp b/src/SharpEmu.Gpu.Vulkan.Native/src/backend.cpp
new file mode 100644
index 0000000..4e21292
--- /dev/null
+++ b/src/SharpEmu.Gpu.Vulkan.Native/src/backend.cpp
@@ -0,0 +1,1329 @@
+/* Copyright (C) 2026 SharpEmu Emulator Project
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+#include "sharpemu_gpu_vulkan.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct guest_image {
+ VkImage image{};
+ VkDeviceMemory memory{};
+ VkImageView view{};
+ VkFormat format{};
+ VkImageLayout layout{VK_IMAGE_LAYOUT_UNDEFINED};
+ uint32_t width{}, height{}, guest_format{}, number_type{};
+ uint64_t cpu_fingerprint{};
+ size_t cpu_size{};
+};
+
+struct host_buffer {
+ VkBuffer buffer{};
+ VkDeviceMemory memory{};
+ VkDeviceSize size{};
+ void* mapping{};
+ VkBufferUsageFlags usage{};
+};
+
+struct se_gpu_backend {
+ SDL_Window* window{}; SDL_Gamepad* gamepad{}; VkInstance instance{}; VkSurfaceKHR surface{};
+ VkPhysicalDevice physical_device{}; VkDevice device{}; VkQueue queue{};
+ uint32_t queue_family{}; std::string error; se_gpu_log_fn log{}; void* log_user{};
+ VkSwapchainKHR swapchain{}; VkFormat swapchain_format{}; VkExtent2D extent{};
+ std::vector swapchain_images;
+ VkCommandPool command_pool{}; VkCommandBuffer command_buffer{};
+ VkPipelineCache pipeline_cache{};
+ VkSemaphore image_available{}, render_finished{}; VkFence frame_fence{};
+ VkBuffer staging{}; VkDeviceMemory staging_memory{}; void* staging_map{};
+ VkDeviceSize staging_capacity{}; bool resized{};
+ std::unordered_map guest_images;
+ std::unordered_map display_formats;
+ std::vector buffer_pool;
+};
+namespace {
+thread_local std::string global_error;
+se_gpu_result ensure_staging(se_gpu_backend* b, VkDeviceSize size);
+se_gpu_result create_swapchain(se_gpu_backend* b);
+se_gpu_result fail(se_gpu_backend* b, se_gpu_result r, std::string message) {
+ (b ? b->error : global_error) = std::move(message);
+ if (b && b->log) b->log(3, b->error.c_str(), b->log_user);
+ return r;
+}
+void cleanup(se_gpu_backend* b) {
+ if (!b) return;
+ if (b->device) {
+ vkDeviceWaitIdle(b->device);
+ for (auto& [address, image] : b->guest_images) {
+ (void)address;
+ if (image.view) vkDestroyImageView(b->device, image.view, nullptr);
+ if (image.image) vkDestroyImage(b->device, image.image, nullptr);
+ if (image.memory) vkFreeMemory(b->device, image.memory, nullptr);
+ }
+ for (auto& buffer : b->buffer_pool) {
+ if (buffer.mapping) vkUnmapMemory(b->device, buffer.memory);
+ if (buffer.buffer) vkDestroyBuffer(b->device, buffer.buffer, nullptr);
+ if (buffer.memory) vkFreeMemory(b->device, buffer.memory, nullptr);
+ }
+ if (b->staging_map) vkUnmapMemory(b->device, b->staging_memory);
+ if (b->staging) vkDestroyBuffer(b->device, b->staging, nullptr);
+ if (b->staging_memory) vkFreeMemory(b->device, b->staging_memory, nullptr);
+ if (b->frame_fence) vkDestroyFence(b->device, b->frame_fence, nullptr);
+ if (b->render_finished) vkDestroySemaphore(b->device, b->render_finished, nullptr);
+ if (b->image_available) vkDestroySemaphore(b->device, b->image_available, nullptr);
+ if (b->command_pool) vkDestroyCommandPool(b->device, b->command_pool, nullptr);
+ if (b->pipeline_cache) vkDestroyPipelineCache(b->device, b->pipeline_cache, nullptr);
+ if (b->swapchain) vkDestroySwapchainKHR(b->device, b->swapchain, nullptr);
+ vkDestroyDevice(b->device, nullptr);
+ }
+ if (b->surface) vkDestroySurfaceKHR(b->instance, b->surface, nullptr);
+ if (b->instance) vkDestroyInstance(b->instance, nullptr);
+ if (b->gamepad) SDL_CloseGamepad(b->gamepad);
+ if (b->window) SDL_DestroyWindow(b->window);
+ SDL_Quit(); delete b;
+}
+
+uint32_t memory_type(se_gpu_backend* b, uint32_t bits, VkMemoryPropertyFlags required) {
+ VkPhysicalDeviceMemoryProperties properties{};
+ vkGetPhysicalDeviceMemoryProperties(b->physical_device, &properties);
+ for (uint32_t index = 0; index < properties.memoryTypeCount; ++index) {
+ if ((bits & (1u << index)) != 0 &&
+ (properties.memoryTypes[index].propertyFlags & required) == required) return index;
+ }
+ return UINT32_MAX;
+}
+
+VkFormat guest_format(uint32_t format, uint32_t number_type) {
+ if (format == 4) return number_type == 4 ? VK_FORMAT_R32_UINT :
+ number_type == 5 ? VK_FORMAT_R32_SINT : VK_FORMAT_R32_SFLOAT;
+ if (format == 5) return number_type == 4 ? VK_FORMAT_R16G16_UINT :
+ number_type == 5 ? VK_FORMAT_R16G16_SINT : number_type == 7 ? VK_FORMAT_R16G16_SFLOAT :
+ VK_FORMAT_R16G16_UNORM;
+ if (format == 10) return number_type == 4 ? VK_FORMAT_R8G8B8A8_UINT :
+ number_type == 5 ? VK_FORMAT_R8G8B8A8_SINT : VK_FORMAT_R8G8B8A8_UNORM;
+ if (format == 12) return number_type == 4 ? VK_FORMAT_R16G16B16A16_UINT :
+ number_type == 5 ? VK_FORMAT_R16G16B16A16_SINT : number_type == 7 ? VK_FORMAT_R16G16B16A16_SFLOAT :
+ VK_FORMAT_R16G16B16A16_UNORM;
+ switch (format) {
+ case 1: case 36: return VK_FORMAT_R8_UNORM;
+ case 3: return VK_FORMAT_R8G8_UNORM;
+ case 7: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
+ case 9: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
+ case 13: case 14: return VK_FORMAT_R32G32B32A32_SFLOAT;
+ case 22: case 71: return VK_FORMAT_R16G16B16A16_SFLOAT;
+ case 29: return VK_FORMAT_R32_SFLOAT;
+ case 49: return VK_FORMAT_R8_UINT;
+ case 56: case 62: case 64: return VK_FORMAT_R8G8B8A8_UNORM;
+ case 75: return VK_FORMAT_R32G32_SFLOAT;
+ case 0x10004: return VK_FORMAT_R32_UINT;
+ case 0x20004: return VK_FORMAT_R32_SINT;
+ case 0x30004: return VK_FORMAT_R32_SFLOAT;
+ case 0x10005: return VK_FORMAT_R16G16_UINT;
+ case 0x20005: return VK_FORMAT_R16G16_SINT;
+ case 0x30005: return VK_FORMAT_R16G16_SFLOAT;
+ case 0x1000a: return VK_FORMAT_R8G8B8A8_UINT;
+ case 0x2000a: return VK_FORMAT_R8G8B8A8_SINT;
+ case 0x1000c: return VK_FORMAT_R16G16B16A16_UINT;
+ case 0x2000c: return VK_FORMAT_R16G16B16A16_SINT;
+ default: return VK_FORMAT_UNDEFINED;
+ }
+}
+
+uint32_t format_bytes(VkFormat format) {
+ switch (format) {
+ case VK_FORMAT_R8_UNORM: case VK_FORMAT_R8_UINT: return 1;
+ case VK_FORMAT_R8G8_UNORM: return 2;
+ case VK_FORMAT_R32G32B32A32_SFLOAT: return 16;
+ case VK_FORMAT_R16G16B16A16_UNORM: case VK_FORMAT_R16G16B16A16_UINT:
+ case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R16G16B16A16_SFLOAT:
+ case VK_FORMAT_R32G32_SFLOAT: return 8;
+ default: return 4;
+ }
+}
+
+[[maybe_unused]] se_gpu_result create_buffer(se_gpu_backend* b, const void* data, size_t data_size,
+ VkBufferUsageFlags usage, host_buffer* output) {
+ if (!output) return SE_GPU_INVALID_ARGUMENT;
+ VkDeviceSize size = std::max(static_cast(data_size), 4);
+ auto pooled = std::find_if(b->buffer_pool.begin(), b->buffer_pool.end(),
+ [size, usage](const host_buffer& candidate) { return candidate.size >= size && candidate.usage == usage; });
+ if (pooled != b->buffer_pool.end()) {
+ *output = *pooled;
+ b->buffer_pool.erase(pooled);
+ if (data && data_size) std::memcpy(output->mapping, data, data_size);
+ return SE_GPU_OK;
+ }
+ VkBufferCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
+ info.size = size; info.usage = usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+ if (vkCreateBuffer(b->device, &info, nullptr, &output->buffer) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "host buffer creation failed");
+ VkMemoryRequirements requirements{}; vkGetBufferMemoryRequirements(b->device, output->buffer, &requirements);
+ uint32_t type = memory_type(b, requirements.memoryTypeBits,
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
+ if (type == UINT32_MAX) return fail(b, SE_GPU_VULKAN_ERROR, "no host buffer memory type");
+ VkMemoryAllocateInfo allocation{}; allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+ allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = type;
+ if (vkAllocateMemory(b->device, &allocation, nullptr, &output->memory) != VK_SUCCESS ||
+ vkBindBufferMemory(b->device, output->buffer, output->memory, 0) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "host buffer memory allocation failed");
+ if (vkMapMemory(b->device, output->memory, 0, size, 0, &output->mapping) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "host buffer map failed");
+ if (data && data_size) std::memcpy(output->mapping, data, data_size);
+ output->size = size;
+ output->usage = usage;
+ return SE_GPU_OK;
+}
+
+[[maybe_unused]] void destroy_buffer(se_gpu_backend* b, host_buffer* buffer) {
+ if (buffer->buffer) b->buffer_pool.push_back(*buffer);
+ *buffer = {};
+}
+
+void destroy_image(se_gpu_backend* b, guest_image* image) {
+ if (image->view) vkDestroyImageView(b->device, image->view, nullptr);
+ if (image->image) vkDestroyImage(b->device, image->image, nullptr);
+ if (image->memory) vkFreeMemory(b->device, image->memory, nullptr);
+ *image = {};
+}
+
+se_gpu_result begin_immediate(se_gpu_backend* b) {
+ if (vkWaitForFences(b->device, 1, &b->frame_fence, VK_TRUE, UINT64_MAX) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "GPU fence wait failed");
+ vkResetFences(b->device, 1, &b->frame_fence);
+ vkResetCommandBuffer(b->command_buffer, 0);
+ VkCommandBufferBeginInfo begin{}; begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+ begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
+ return vkBeginCommandBuffer(b->command_buffer, &begin) == VK_SUCCESS ? SE_GPU_OK :
+ fail(b, SE_GPU_VULKAN_ERROR, "command buffer begin failed");
+}
+
+se_gpu_result end_immediate(se_gpu_backend* b) {
+ if (vkEndCommandBuffer(b->command_buffer) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "command buffer end failed");
+ VkSubmitInfo submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+ submit.commandBufferCount = 1; submit.pCommandBuffers = &b->command_buffer;
+ if (vkQueueSubmit(b->queue, 1, &submit, b->frame_fence) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "GPU queue submit failed");
+ return vkWaitForFences(b->device, 1, &b->frame_fence, VK_TRUE, UINT64_MAX) == VK_SUCCESS ? SE_GPU_OK :
+ fail(b, SE_GPU_VULKAN_ERROR, "GPU command completion failed");
+}
+
+void image_barrier(se_gpu_backend* b, guest_image* image, VkImageLayout next,
+ VkAccessFlags source_access, VkAccessFlags destination_access,
+ VkPipelineStageFlags source_stage, VkPipelineStageFlags destination_stage) {
+ VkImageMemoryBarrier barrier{}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
+ barrier.srcAccessMask = source_access; barrier.dstAccessMask = destination_access;
+ barrier.oldLayout = image->layout; barrier.newLayout = next;
+ barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+ barrier.image = image->image; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
+ vkCmdPipelineBarrier(b->command_buffer, source_stage, destination_stage, 0,
+ 0, nullptr, 0, nullptr, 1, &barrier);
+ image->layout = next;
+}
+
+se_gpu_result create_image(se_gpu_backend* b, uint32_t width, uint32_t height,
+ uint32_t format, uint32_t number_type, guest_image* output) {
+ VkFormat native_format = guest_format(format, number_type);
+ if (!width || !height || native_format == VK_FORMAT_UNDEFINED)
+ return fail(b, SE_GPU_INVALID_ARGUMENT, "unsupported guest image format");
+ VkImageCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
+ info.imageType = VK_IMAGE_TYPE_2D; info.format = native_format; info.extent = {width, height, 1};
+ info.mipLevels = 1; info.arrayLayers = 1; info.samples = VK_SAMPLE_COUNT_1_BIT;
+ info.tiling = VK_IMAGE_TILING_OPTIMAL; info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+ VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT |
+ VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+ if (vkCreateImage(b->device, &info, nullptr, &output->image) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "guest image creation failed");
+ VkMemoryRequirements requirements{}; vkGetImageMemoryRequirements(b->device, output->image, &requirements);
+ uint32_t type = memory_type(b, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ if (type == UINT32_MAX) return fail(b, SE_GPU_VULKAN_ERROR, "no device-local image memory type");
+ VkMemoryAllocateInfo allocation{}; allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+ allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = type;
+ if (vkAllocateMemory(b->device, &allocation, nullptr, &output->memory) != VK_SUCCESS ||
+ vkBindImageMemory(b->device, output->image, output->memory, 0) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "guest image memory allocation failed");
+ VkImageViewCreateInfo view{}; view.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
+ view.image = output->image; view.viewType = VK_IMAGE_VIEW_TYPE_2D; view.format = native_format;
+ view.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
+ if (vkCreateImageView(b->device, &view, nullptr, &output->view) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "guest image view creation failed");
+ output->format = native_format; output->width = width; output->height = height;
+ output->guest_format = format; output->number_type = number_type; return SE_GPU_OK;
+}
+
+se_gpu_result upload_image(se_gpu_backend* b, guest_image* image, const se_gpu_bytes& pixels,
+ uint32_t row_length) {
+ if (!pixels.data || !pixels.size) return SE_GPU_OK;
+ row_length = std::max(row_length, image->width);
+ uint32_t source_texel_bytes = image->guest_format == 13 ? 12u : format_bytes(image->format);
+ uint32_t upload_texel_bytes = format_bytes(image->format);
+ size_t required = static_cast(row_length) * image->height * source_texel_bytes;
+ if (pixels.size < required) return fail(b, SE_GPU_INVALID_ARGUMENT, "guest image upload is undersized");
+ size_t upload_size = static_cast(row_length) * image->height * upload_texel_bytes;
+ se_gpu_result result = ensure_staging(b, static_cast(upload_size));
+ if (result != SE_GPU_OK) return result;
+ if (image->guest_format == 13) {
+ const auto* source = static_cast(pixels.data);
+ auto* destination = static_cast(b->staging_map);
+ size_t texels = static_cast(row_length) * image->height;
+ for (size_t texel = 0; texel < texels; ++texel) {
+ std::memcpy(destination + texel * 16, source + texel * 12, 12);
+ destination[texel * 16 + 12] = 0;
+ destination[texel * 16 + 13] = 0;
+ destination[texel * 16 + 14] = 0x80;
+ destination[texel * 16 + 15] = 0x3f;
+ }
+ } else {
+ std::memcpy(b->staging_map, pixels.data, upload_size);
+ }
+ result = begin_immediate(b); if (result != SE_GPU_OK) return result;
+ image_barrier(b, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT,
+ VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+ VkBufferImageCopy copy{};
+ copy.bufferRowLength = row_length > image->width ? row_length : 0;
+ copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
+ copy.imageExtent = {image->width, image->height, 1};
+ vkCmdCopyBufferToImage(b->command_buffer, b->staging, image->image,
+ VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
+ image_barrier(b, image, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_WRITE_BIT,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
+ return end_immediate(b);
+}
+
+uint64_t fingerprint(const se_gpu_bytes& bytes) {
+ const auto* data = static_cast(bytes.data);
+ uint64_t hash = 1469598103934665603ull;
+ for (size_t index = 0; index < bytes.size; ++index) {
+ hash ^= data[index]; hash *= 1099511628211ull;
+ }
+ return hash;
+}
+
+se_gpu_result cached_image(se_gpu_backend* b, uint64_t address, uint32_t width, uint32_t height,
+ uint32_t format, uint32_t number_type, uint32_t row_length,
+ const se_gpu_bytes* pixels, guest_image** output) {
+ if (!address || !output) return SE_GPU_INVALID_ARGUMENT;
+ auto [iterator, inserted] = b->guest_images.try_emplace(address);
+ guest_image& image = iterator->second;
+ VkFormat native_format = guest_format(format, number_type);
+ if (!inserted && (image.width != width || image.height != height || image.format != native_format)) {
+ vkDeviceWaitIdle(b->device); destroy_image(b, &image); inserted = true;
+ }
+ se_gpu_result result = SE_GPU_OK;
+ if (inserted) result = create_image(b, width, height, format, number_type, &image);
+ if (result == SE_GPU_OK && pixels && pixels->data && pixels->size) {
+ uint64_t content = fingerprint(*pixels);
+ if (image.cpu_size != pixels->size || image.cpu_fingerprint != content) {
+ result = upload_image(b, &image, *pixels, row_length);
+ if (result == SE_GPU_OK) { image.cpu_size = pixels->size; image.cpu_fingerprint = content; }
+ }
+ }
+ if (result != SE_GPU_OK) { destroy_image(b, &image); b->guest_images.erase(iterator); return result; }
+ *output = ℑ return SE_GPU_OK;
+}
+
+se_gpu_result blit_image(se_gpu_backend* b, guest_image* source, guest_image* destination) {
+ se_gpu_result result = begin_immediate(b); if (result != SE_GPU_OK) return result;
+ VkImageLayout source_layout = source->layout;
+ image_barrier(b, source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+ VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+ image_barrier(b, destination, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+ VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+ VkImageBlit region{}; region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
+ region.srcOffsets[1] = {static_cast(source->width), static_cast(source->height), 1};
+ region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
+ region.dstOffsets[1] = {static_cast(destination->width), static_cast(destination->height), 1};
+ vkCmdBlitImage(b->command_buffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ destination->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_NEAREST);
+ image_barrier(b, source, source_layout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : source_layout,
+ VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
+ image_barrier(b, destination, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_WRITE_BIT,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
+ return end_immediate(b);
+}
+
+se_gpu_result present_image(se_gpu_backend* b, guest_image* source) {
+ if (vkWaitForFences(b->device, 1, &b->frame_fence, VK_TRUE, UINT64_MAX) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "presentation fence wait failed");
+ if (b->resized) { vkDeviceWaitIdle(b->device); se_gpu_result recreated = create_swapchain(b);
+ if (recreated != SE_GPU_OK) return recreated; }
+ uint32_t image_index{}; VkResult acquired = vkAcquireNextImageKHR(b->device, b->swapchain,
+ UINT64_MAX, b->image_available, VK_NULL_HANDLE, &image_index);
+ if (acquired == VK_ERROR_OUT_OF_DATE_KHR) { b->resized = true; return SE_GPU_NOT_READY; }
+ if (acquired != VK_SUCCESS && acquired != VK_SUBOPTIMAL_KHR)
+ return fail(b, SE_GPU_VULKAN_ERROR, "guest image swapchain acquisition failed");
+ vkResetFences(b->device, 1, &b->frame_fence); vkResetCommandBuffer(b->command_buffer, 0);
+ VkCommandBufferBeginInfo begin{}; begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+ begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
+ if (vkBeginCommandBuffer(b->command_buffer, &begin) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "guest image command begin failed");
+ VkImageLayout original = source->layout;
+ image_barrier(b, source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+ VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+ VkImageMemoryBarrier swapchain{}; swapchain.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
+ swapchain.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; swapchain.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+ swapchain.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
+ swapchain.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; swapchain.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+ swapchain.image = b->swapchain_images[image_index];
+ swapchain.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
+ vkCmdPipelineBarrier(b->command_buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
+ 0, 0, nullptr, 0, nullptr, 1, &swapchain);
+ VkImageBlit region{}; region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
+ region.srcOffsets[1] = {static_cast(source->width), static_cast(source->height), 1};
+ region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
+ region.dstOffsets[1] = {static_cast(b->extent.width), static_cast(b->extent.height), 1};
+ vkCmdBlitImage(b->command_buffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ b->swapchain_images[image_index], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_NEAREST);
+ image_barrier(b, source, original == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : original,
+ VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
+ swapchain.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; swapchain.dstAccessMask = 0;
+ swapchain.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; swapchain.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
+ vkCmdPipelineBarrier(b->command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
+ 0, 0, nullptr, 0, nullptr, 1, &swapchain);
+ if (vkEndCommandBuffer(b->command_buffer) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "guest image command end failed");
+ VkPipelineStageFlags wait = VK_PIPELINE_STAGE_TRANSFER_BIT; VkSubmitInfo submit{};
+ submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submit.waitSemaphoreCount = 1;
+ submit.pWaitSemaphores = &b->image_available; submit.pWaitDstStageMask = &wait;
+ submit.commandBufferCount = 1; submit.pCommandBuffers = &b->command_buffer;
+ submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &b->render_finished;
+ if (vkQueueSubmit(b->queue, 1, &submit, b->frame_fence) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "guest image queue submit failed");
+ VkPresentInfoKHR present{}; present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
+ present.waitSemaphoreCount = 1; present.pWaitSemaphores = &b->render_finished;
+ present.swapchainCount = 1; present.pSwapchains = &b->swapchain; present.pImageIndices = &image_index;
+ VkResult result = vkQueuePresentKHR(b->queue, &present);
+ if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { b->resized = true; return SE_GPU_OK; }
+ if (result != VK_SUCCESS) return fail(b, SE_GPU_VULKAN_ERROR, "guest image present failed");
+ return vkWaitForFences(b->device, 1, &b->frame_fence, VK_TRUE, UINT64_MAX) == VK_SUCCESS ? SE_GPU_OK :
+ fail(b, SE_GPU_VULKAN_ERROR, "guest image presentation completion failed");
+}
+
+se_gpu_result create_commands(se_gpu_backend* b) {
+ VkPipelineCacheCreateInfo cache{}; cache.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
+ if (vkCreatePipelineCache(b->device, &cache, nullptr, &b->pipeline_cache) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "vkCreatePipelineCache failed");
+ VkCommandPoolCreateInfo pool{};
+ pool.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
+ pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
+ pool.queueFamilyIndex = b->queue_family;
+ if (vkCreateCommandPool(b->device, &pool, nullptr, &b->command_pool) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "vkCreateCommandPool failed");
+ VkCommandBufferAllocateInfo allocate{};
+ allocate.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
+ allocate.commandPool = b->command_pool; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
+ allocate.commandBufferCount = 1;
+ if (vkAllocateCommandBuffers(b->device, &allocate, &b->command_buffer) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "vkAllocateCommandBuffers failed");
+ VkSemaphoreCreateInfo semaphore{}; semaphore.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
+ if (vkCreateSemaphore(b->device, &semaphore, nullptr, &b->image_available) != VK_SUCCESS ||
+ vkCreateSemaphore(b->device, &semaphore, nullptr, &b->render_finished) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "vkCreateSemaphore failed");
+ VkFenceCreateInfo fence{}; fence.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
+ fence.flags = VK_FENCE_CREATE_SIGNALED_BIT;
+ if (vkCreateFence(b->device, &fence, nullptr, &b->frame_fence) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "vkCreateFence failed");
+ return SE_GPU_OK;
+}
+
+se_gpu_result create_swapchain(se_gpu_backend* b) {
+ VkSurfaceCapabilitiesKHR capabilities{};
+ if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(b->physical_device, b->surface, &capabilities) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "surface capability query failed");
+ int width{}, height{}; SDL_GetWindowSizeInPixels(b->window, &width, &height);
+ if (width <= 0 || height <= 0) return SE_GPU_NOT_READY;
+ uint32_t count{}; vkGetPhysicalDeviceSurfaceFormatsKHR(b->physical_device, b->surface, &count, nullptr);
+ if (!count) return fail(b, SE_GPU_VULKAN_ERROR, "surface has no formats");
+ std::vector formats(count);
+ vkGetPhysicalDeviceSurfaceFormatsKHR(b->physical_device, b->surface, &count, formats.data());
+ VkSurfaceFormatKHR selected = formats.front();
+
+ // Guest render targets are already encoded for display. Prefer a UNORM
+ // swapchain everywhere so presentation does not apply an additional sRGB
+ // transfer function. BGRA avoids a channel shuffle on the common path;
+ // RGBA UNORM is the next-best native surface format.
+ for (const auto& format : formats) {
+ if (format.format == VK_FORMAT_B8G8R8A8_UNORM) {
+ selected = format; break;
+ }
+ }
+ if (selected.format != VK_FORMAT_B8G8R8A8_UNORM) {
+ for (const auto& format : formats) {
+ if (format.format == VK_FORMAT_R8G8B8A8_UNORM) {
+ selected = format; break;
+ }
+ }
+ }
+ VkExtent2D extent = capabilities.currentExtent;
+ if (extent.width == UINT32_MAX) {
+ extent.width = std::clamp(static_cast(width), capabilities.minImageExtent.width,
+ capabilities.maxImageExtent.width);
+ extent.height = std::clamp(static_cast(height), capabilities.minImageExtent.height,
+ capabilities.maxImageExtent.height);
+ }
+ uint32_t image_count = capabilities.minImageCount + 1;
+ if (capabilities.maxImageCount) image_count = std::min(image_count, capabilities.maxImageCount);
+ VkCompositeAlphaFlagBitsKHR alpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
+ if ((capabilities.supportedCompositeAlpha & alpha) == 0) {
+ const VkCompositeAlphaFlagBitsKHR choices[]{VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
+ VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR};
+ for (auto choice : choices) if ((capabilities.supportedCompositeAlpha & choice) != 0) { alpha = choice; break; }
+ }
+ VkSwapchainCreateInfoKHR info{}; info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
+ info.surface = b->surface; info.minImageCount = image_count; info.imageFormat = selected.format;
+ info.imageColorSpace = selected.colorSpace; info.imageExtent = extent; info.imageArrayLayers = 1;
+ info.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
+ info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; info.preTransform = capabilities.currentTransform;
+ info.compositeAlpha = alpha; info.presentMode = VK_PRESENT_MODE_FIFO_KHR; info.clipped = VK_TRUE;
+ info.oldSwapchain = b->swapchain;
+ VkSwapchainKHR replacement{}; VkResult result = vkCreateSwapchainKHR(b->device, &info, nullptr, &replacement);
+ if (result != VK_SUCCESS) return fail(b, SE_GPU_VULKAN_ERROR, "vkCreateSwapchainKHR failed");
+ if (b->swapchain) vkDestroySwapchainKHR(b->device, b->swapchain, nullptr);
+ b->swapchain = replacement; b->swapchain_format = selected.format; b->extent = extent;
+ vkGetSwapchainImagesKHR(b->device, b->swapchain, &image_count, nullptr);
+ b->swapchain_images.resize(image_count);
+ vkGetSwapchainImagesKHR(b->device, b->swapchain, &image_count, b->swapchain_images.data());
+ b->resized = false; return SE_GPU_OK;
+}
+
+se_gpu_result ensure_staging(se_gpu_backend* b, VkDeviceSize size) {
+ if (b->staging_capacity >= size) return SE_GPU_OK;
+ vkDeviceWaitIdle(b->device);
+ if (b->staging_map) vkUnmapMemory(b->device, b->staging_memory);
+ if (b->staging) vkDestroyBuffer(b->device, b->staging, nullptr);
+ if (b->staging_memory) vkFreeMemory(b->device, b->staging_memory, nullptr);
+ b->staging_map = nullptr; b->staging = {}; b->staging_memory = {}; b->staging_capacity = 0;
+ VkBufferCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; info.size = size;
+ info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+ if (vkCreateBuffer(b->device, &info, nullptr, &b->staging) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "staging buffer creation failed");
+ VkMemoryRequirements requirements{}; vkGetBufferMemoryRequirements(b->device, b->staging, &requirements);
+ uint32_t type = memory_type(b, requirements.memoryTypeBits,
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
+ if (type == UINT32_MAX) return fail(b, SE_GPU_VULKAN_ERROR, "no host-visible memory type");
+ VkMemoryAllocateInfo allocation{}; allocation.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+ allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = type;
+ if (vkAllocateMemory(b->device, &allocation, nullptr, &b->staging_memory) != VK_SUCCESS ||
+ vkBindBufferMemory(b->device, b->staging, b->staging_memory, 0) != VK_SUCCESS ||
+ vkMapMemory(b->device, b->staging_memory, 0, requirements.size, 0, &b->staging_map) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "staging memory allocation failed");
+ b->staging_capacity = requirements.size; return SE_GPU_OK;
+}
+
+void refresh_gamepad(se_gpu_backend* b) {
+ if (b->gamepad && SDL_GamepadConnected(b->gamepad)) return;
+ if (b->gamepad) { SDL_CloseGamepad(b->gamepad); b->gamepad = nullptr; }
+ int count{}; SDL_JoystickID* ids = SDL_GetGamepads(&count);
+ if (ids && count > 0) b->gamepad = SDL_OpenGamepad(ids[0]);
+ SDL_free(ids);
+}
+void set_key(se_gpu_input* state, uint32_t virtual_key, bool down) {
+ if (down && virtual_key < 256) state->virtual_keys[virtual_key / 32] |= 1u << (virtual_key % 32);
+}
+uint8_t stick_value(int16_t value) {
+ return static_cast((static_cast(value) + 32768) * 255 / 65535);
+}
+uint8_t trigger_value(int16_t value) {
+ return static_cast(std::clamp(static_cast(value), 0, 32767) * 255 / 32767);
+}
+
+VkPrimitiveTopology primitive_topology(uint32_t type) {
+ switch (type) {
+ case 1: return VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
+ case 2: return VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
+ case 3: return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
+ case 5: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
+ case 6: case 0x11: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
+ default: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
+ }
+}
+VkBlendFactor blend_factor(uint32_t value) {
+ switch (value) {
+ case 0: return VK_BLEND_FACTOR_ZERO; case 1: return VK_BLEND_FACTOR_ONE;
+ case 2: return VK_BLEND_FACTOR_SRC_COLOR; case 3: return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR;
+ case 4: return VK_BLEND_FACTOR_SRC_ALPHA; case 5: return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
+ case 6: return VK_BLEND_FACTOR_DST_ALPHA; case 7: return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA;
+ case 8: return VK_BLEND_FACTOR_DST_COLOR; case 9: return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR;
+ case 10: return VK_BLEND_FACTOR_SRC_ALPHA_SATURATE; case 13: return VK_BLEND_FACTOR_CONSTANT_COLOR;
+ case 14: return VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR; case 15: return VK_BLEND_FACTOR_SRC1_COLOR;
+ case 16: return VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR; case 17: return VK_BLEND_FACTOR_SRC1_ALPHA;
+ case 18: return VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA; case 19: return VK_BLEND_FACTOR_CONSTANT_ALPHA;
+ case 20: return VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA; default: return VK_BLEND_FACTOR_ONE;
+ }
+}
+VkBlendOp blend_op(uint32_t value) {
+ switch (value) { case 1: return VK_BLEND_OP_SUBTRACT; case 2: return VK_BLEND_OP_MIN;
+ case 3: return VK_BLEND_OP_MAX; case 4: return VK_BLEND_OP_REVERSE_SUBTRACT; default: return VK_BLEND_OP_ADD; }
+}
+VkFormat vertex_format(uint32_t data, uint32_t number, uint32_t components) {
+ if (data == 1) return number == 1 ? VK_FORMAT_R8_SNORM : number == 4 ? VK_FORMAT_R8_UINT :
+ number == 5 ? VK_FORMAT_R8_SINT : number == 9 ? VK_FORMAT_R8_SRGB : VK_FORMAT_R8_UNORM;
+ if (data == 2) return number == 1 ? VK_FORMAT_R16_SNORM : number == 4 ? VK_FORMAT_R16_UINT :
+ number == 5 ? VK_FORMAT_R16_SINT : number == 7 ? VK_FORMAT_R16_SFLOAT : VK_FORMAT_R16_UNORM;
+ if (data == 3) return number == 1 ? VK_FORMAT_R8G8_SNORM : number == 4 ? VK_FORMAT_R8G8_UINT :
+ number == 5 ? VK_FORMAT_R8G8_SINT : number == 9 ? VK_FORMAT_R8G8_SRGB : VK_FORMAT_R8G8_UNORM;
+ if (data == 4) return number == 4 ? VK_FORMAT_R32_UINT : number == 5 ? VK_FORMAT_R32_SINT : VK_FORMAT_R32_SFLOAT;
+ if (data == 5) return number == 1 ? VK_FORMAT_R16G16_SNORM : number == 4 ? VK_FORMAT_R16G16_UINT :
+ number == 5 ? VK_FORMAT_R16G16_SINT : number == 7 ? VK_FORMAT_R16G16_SFLOAT : VK_FORMAT_R16G16_UNORM;
+ if (data == 10) return number == 1 ? VK_FORMAT_R8G8B8A8_SNORM : number == 4 ? VK_FORMAT_R8G8B8A8_UINT :
+ number == 5 ? VK_FORMAT_R8G8B8A8_SINT : number == 9 ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM;
+ if (data == 11) return number == 4 ? VK_FORMAT_R32G32_UINT : number == 5 ? VK_FORMAT_R32G32_SINT : VK_FORMAT_R32G32_SFLOAT;
+ if (data == 12) return number == 1 || number == 6 ? VK_FORMAT_R16G16B16A16_SNORM :
+ number == 4 ? VK_FORMAT_R16G16B16A16_UINT : number == 5 ? VK_FORMAT_R16G16B16A16_SINT :
+ number == 7 ? VK_FORMAT_R16G16B16A16_SFLOAT : VK_FORMAT_R16G16B16A16_UNORM;
+ if (data == 13) return number == 4 ? VK_FORMAT_R32G32B32_UINT : number == 5 ? VK_FORMAT_R32G32B32_SINT : VK_FORMAT_R32G32B32_SFLOAT;
+ if (data == 14) return number == 4 ? VK_FORMAT_R32G32B32A32_UINT : number == 5 ? VK_FORMAT_R32G32B32A32_SINT : VK_FORMAT_R32G32B32A32_SFLOAT;
+ switch (components) { case 2: return VK_FORMAT_R32G32_SFLOAT; case 3: return VK_FORMAT_R32G32B32_SFLOAT;
+ case 4: return VK_FORMAT_R32G32B32A32_SFLOAT; default: return VK_FORMAT_R32_SFLOAT; }
+}
+
+VkSamplerAddressMode sampler_address(uint32_t value) {
+ switch (value) { case 0: return VK_SAMPLER_ADDRESS_MODE_REPEAT;
+ case 1: return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
+ case 2: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ case 3: case 5: case 7: return VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE;
+ case 4: case 6: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
+ default: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; }
+}
+VkCompareOp compare_op(uint32_t value) {
+ switch (value) { case 1: return VK_COMPARE_OP_LESS; case 2: return VK_COMPARE_OP_EQUAL;
+ case 3: return VK_COMPARE_OP_LESS_OR_EQUAL; case 4: return VK_COMPARE_OP_GREATER;
+ case 5: return VK_COMPARE_OP_NOT_EQUAL; case 6: return VK_COMPARE_OP_GREATER_OR_EQUAL;
+ case 7: return VK_COMPARE_OP_ALWAYS; default: return VK_COMPARE_OP_NEVER; }
+}
+se_gpu_result create_sampler(se_gpu_backend* b, const se_gpu_sampler& source, VkSampler* output) {
+ uint32_t compare = (source.words[0] >> 12) & 7u;
+ uint32_t border = source.words[3] >> 30;
+ uint32_t bias_bits = source.words[2] & 0x3fffu;
+ int32_t signed_bias = static_cast((bias_bits ^ 0x2000u) - 0x2000u);
+ VkSamplerCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
+ info.addressModeU = sampler_address(source.words[0] & 7u);
+ info.addressModeV = sampler_address((source.words[0] >> 3) & 7u);
+ info.addressModeW = sampler_address((source.words[0] >> 6) & 7u);
+ info.magFilter = ((source.words[2] >> 20) & 3u) == 1 || ((source.words[2] >> 20) & 3u) == 3 ?
+ VK_FILTER_LINEAR : VK_FILTER_NEAREST;
+ info.minFilter = ((source.words[2] >> 22) & 3u) == 1 || ((source.words[2] >> 22) & 3u) == 3 ?
+ VK_FILTER_LINEAR : VK_FILTER_NEAREST;
+ info.mipmapMode = ((source.words[2] >> 26) & 3u) == 2 ?
+ VK_SAMPLER_MIPMAP_MODE_LINEAR : VK_SAMPLER_MIPMAP_MODE_NEAREST;
+ info.mipLodBias = static_cast(signed_bias) / 256.0f;
+ info.minLod = static_cast(source.words[1] & 0xfffu) / 256.0f;
+ info.maxLod = std::max(info.minLod,
+ static_cast((source.words[1] >> 12) & 0xfffu) / 256.0f);
+ info.compareEnable = compare ? VK_TRUE : VK_FALSE; info.compareOp = compare_op(compare);
+ info.borderColor = border == 1 ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK :
+ border == 2 ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
+ return vkCreateSampler(b->device, &info, nullptr, output) == VK_SUCCESS ? SE_GPU_OK :
+ fail(b, SE_GPU_VULKAN_ERROR, "guest sampler creation failed");
+}
+
+VkComponentSwizzle component_swizzle(uint32_t selector) {
+ switch (selector) { case 0: return VK_COMPONENT_SWIZZLE_ZERO; case 1: return VK_COMPONENT_SWIZZLE_ONE;
+ case 4: return VK_COMPONENT_SWIZZLE_R; case 5: return VK_COMPONENT_SWIZZLE_G;
+ case 6: return VK_COMPONENT_SWIZZLE_B; case 7: return VK_COMPONENT_SWIZZLE_A;
+ default: return VK_COMPONENT_SWIZZLE_IDENTITY; }
+}
+se_gpu_result create_texture_view(se_gpu_backend* b, guest_image* image,
+ const se_gpu_texture& texture, VkImageView* output) {
+ uint32_t select = texture.dst_select ? texture.dst_select : 0xfacu;
+ if (texture.is_storage || (select == 0xfacu && texture.mip_level == 0)) {
+ *output = image->view; return SE_GPU_OK;
+ }
+ if (texture.mip_level != 0)
+ return fail(b, SE_GPU_INVALID_ARGUMENT, "guest texture requests an unavailable mip level");
+ VkImageViewCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
+ info.image = image->image; info.viewType = VK_IMAGE_VIEW_TYPE_2D; info.format = image->format;
+ info.components = {component_swizzle(select & 7u), component_swizzle((select >> 3) & 7u),
+ component_swizzle((select >> 6) & 7u), component_swizzle((select >> 9) & 7u)};
+ info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
+ return vkCreateImageView(b->device, &info, nullptr, output) == VK_SUCCESS ? SE_GPU_OK :
+ fail(b, SE_GPU_VULKAN_ERROR, "swizzled guest texture view creation failed");
+}
+}
+extern "C" {
+uint32_t SE_GPU_CALL se_gpu_abi_version(void) { return SE_GPU_ABI_VERSION; }
+const char* SE_GPU_CALL se_gpu_last_error(const se_gpu_backend* b) {
+ return b ? b->error.c_str() : global_error.c_str();
+}
+se_gpu_result SE_GPU_CALL se_gpu_create(const se_gpu_create_info* info, se_gpu_backend** out) {
+ if (!info || !out || info->struct_size < sizeof(*info) || !info->width || !info->height)
+ return SE_GPU_INVALID_ARGUMENT;
+ *out = nullptr;
+ if (info->abi_version != SE_GPU_ABI_VERSION) return SE_GPU_INCOMPATIBLE_ABI;
+ auto* b = new (std::nothrow) se_gpu_backend{};
+ if (!b) return SE_GPU_OUT_OF_MEMORY;
+ b->log = info->log; b->log_user = info->log_user;
+ if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) { cleanup(b); return fail(nullptr, SE_GPU_PLATFORM_ERROR, SDL_GetError()); }
+ b->window = SDL_CreateWindow(info->title_utf8 ? info->title_utf8 : "SharpEmu",
+ static_cast(info->width), static_cast(info->height), SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);
+ if (!b->window) { cleanup(b); return fail(nullptr, SE_GPU_PLATFORM_ERROR, SDL_GetError()); }
+ uint32_t count{}; const char* const* extensions = SDL_Vulkan_GetInstanceExtensions(&count);
+ VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO, nullptr, "SharpEmu", 1,
+ "SharpEmu native guest GPU", 1, VK_API_VERSION_1_2};
+ VkInstanceCreateInfo create{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, nullptr, 0, &app,
+ 0, nullptr, count, extensions};
+ VkResult vr = vkCreateInstance(&create, nullptr, &b->instance);
+ if (vr != VK_SUCCESS) { cleanup(b); return fail(nullptr, SE_GPU_VULKAN_ERROR, "vkCreateInstance failed"); }
+ if (!SDL_Vulkan_CreateSurface(b->window, b->instance, nullptr, &b->surface)) {
+ cleanup(b); return fail(nullptr, SE_GPU_VULKAN_ERROR, SDL_GetError());
+ }
+ uint32_t device_count{}; vkEnumeratePhysicalDevices(b->instance, &device_count, nullptr);
+ if (!device_count) { cleanup(b); return fail(nullptr, SE_GPU_VULKAN_ERROR, "no Vulkan device"); }
+ VkPhysicalDevice devices[16]{}; device_count = device_count > 16 ? 16 : device_count;
+ vkEnumeratePhysicalDevices(b->instance, &device_count, devices);
+ for (uint32_t d = 0; d < device_count && !b->physical_device; ++d) {
+ uint32_t families{}; vkGetPhysicalDeviceQueueFamilyProperties(devices[d], &families, nullptr);
+ std::vector properties(families);
+ vkGetPhysicalDeviceQueueFamilyProperties(devices[d], &families, properties.data());
+ for (uint32_t q = 0; q < families; ++q) { VkBool32 present{};
+ vkGetPhysicalDeviceSurfaceSupportKHR(devices[d], q, b->surface, &present);
+ if (present && (properties[q].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0) {
+ b->physical_device = devices[d]; b->queue_family = q; break;
+ }
+ }
+ }
+ if (!b->physical_device) { cleanup(b); return fail(nullptr, SE_GPU_VULKAN_ERROR, "no presentation queue"); }
+ float priority = 1; VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
+ nullptr, 0, b->queue_family, 1, &priority};
+ const char* device_extensions[]{VK_KHR_SWAPCHAIN_EXTENSION_NAME};
+ VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, nullptr, 0, 1, &queue,
+ 0, nullptr, 1, device_extensions, nullptr};
+ vr = vkCreateDevice(b->physical_device, &device, nullptr, &b->device);
+ if (vr != VK_SUCCESS) { cleanup(b); return fail(nullptr, SE_GPU_VULKAN_ERROR, "vkCreateDevice failed"); }
+ vkGetDeviceQueue(b->device, b->queue_family, 0, &b->queue);
+ se_gpu_result result = create_commands(b);
+ if (result == SE_GPU_OK) result = create_swapchain(b);
+ if (result != SE_GPU_OK) { global_error = b->error; cleanup(b); return result; }
+ *out = b; return SE_GPU_OK;
+}
+void SE_GPU_CALL se_gpu_destroy(se_gpu_backend* b) { cleanup(b); }
+se_gpu_result SE_GPU_CALL se_gpu_poll(se_gpu_backend* b, uint32_t* close) {
+ if (!b || !close) return SE_GPU_INVALID_ARGUMENT;
+ *close = 0;
+ SDL_Event e{};
+ while (SDL_PollEvent(&e)) {
+ if (e.type == SDL_EVENT_QUIT || e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) *close = 1;
+ if (e.type == SDL_EVENT_WINDOW_RESIZED || e.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) b->resized = true;
+ }
+ return SE_GPU_OK;
+}
+se_gpu_result SE_GPU_CALL se_gpu_input_snapshot(se_gpu_backend* b, se_gpu_input* state) {
+ if (!b || !state || state->struct_size < sizeof(*state)) return SE_GPU_INVALID_ARGUMENT;
+ uint32_t size = state->struct_size; std::memset(state, 0, sizeof(*state)); state->struct_size = size;
+ state->keyboard_focused = (SDL_GetWindowFlags(b->window) & SDL_WINDOW_INPUT_FOCUS) != 0 ? 1u : 0u;
+ int count{}; const bool* keys = SDL_GetKeyboardState(&count);
+ auto down = [keys, count](SDL_Scancode key) { return static_cast(key) < count && keys[key]; };
+ set_key(state, 0x08, down(SDL_SCANCODE_BACKSPACE)); set_key(state, 0x09, down(SDL_SCANCODE_TAB));
+ set_key(state, 0x0d, down(SDL_SCANCODE_RETURN)); set_key(state, 0x1b, down(SDL_SCANCODE_ESCAPE));
+ set_key(state, 0x25, down(SDL_SCANCODE_LEFT)); set_key(state, 0x26, down(SDL_SCANCODE_UP));
+ set_key(state, 0x27, down(SDL_SCANCODE_RIGHT)); set_key(state, 0x28, down(SDL_SCANCODE_DOWN));
+ for (uint32_t index = 0; index < 26; ++index)
+ set_key(state, 0x41u + index, down(static_cast(SDL_SCANCODE_A + index)));
+ refresh_gamepad(b); if (!b->gamepad) return SE_GPU_OK;
+ state->gamepad_connected = 1;
+ auto button = [b](SDL_GamepadButton value) { return SDL_GetGamepadButton(b->gamepad, value); };
+ if (button(SDL_GAMEPAD_BUTTON_DPAD_UP)) state->gamepad_buttons |= 1u << 0;
+ if (button(SDL_GAMEPAD_BUTTON_DPAD_DOWN)) state->gamepad_buttons |= 1u << 1;
+ if (button(SDL_GAMEPAD_BUTTON_DPAD_LEFT)) state->gamepad_buttons |= 1u << 2;
+ if (button(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) state->gamepad_buttons |= 1u << 3;
+ if (button(SDL_GAMEPAD_BUTTON_SOUTH)) state->gamepad_buttons |= 1u << 4;
+ if (button(SDL_GAMEPAD_BUTTON_EAST)) state->gamepad_buttons |= 1u << 5;
+ if (button(SDL_GAMEPAD_BUTTON_WEST)) state->gamepad_buttons |= 1u << 6;
+ if (button(SDL_GAMEPAD_BUTTON_NORTH)) state->gamepad_buttons |= 1u << 7;
+ if (button(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) state->gamepad_buttons |= 1u << 8;
+ if (button(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) state->gamepad_buttons |= 1u << 9;
+ if (button(SDL_GAMEPAD_BUTTON_LEFT_STICK)) state->gamepad_buttons |= 1u << 12;
+ if (button(SDL_GAMEPAD_BUTTON_RIGHT_STICK)) state->gamepad_buttons |= 1u << 13;
+ if (button(SDL_GAMEPAD_BUTTON_START)) state->gamepad_buttons |= 1u << 14;
+ if (button(SDL_GAMEPAD_BUTTON_BACK)) state->gamepad_buttons |= 1u << 15;
+ state->left_x = stick_value(SDL_GetGamepadAxis(b->gamepad, SDL_GAMEPAD_AXIS_LEFTX));
+ state->left_y = stick_value(SDL_GetGamepadAxis(b->gamepad, SDL_GAMEPAD_AXIS_LEFTY));
+ state->right_x = stick_value(SDL_GetGamepadAxis(b->gamepad, SDL_GAMEPAD_AXIS_RIGHTX));
+ state->right_y = stick_value(SDL_GetGamepadAxis(b->gamepad, SDL_GAMEPAD_AXIS_RIGHTY));
+ state->left_trigger = trigger_value(SDL_GetGamepadAxis(b->gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER));
+ state->right_trigger = trigger_value(SDL_GetGamepadAxis(b->gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER));
+ if (state->left_trigger > 64) state->gamepad_buttons |= 1u << 10;
+ if (state->right_trigger > 64) state->gamepad_buttons |= 1u << 11;
+ const char* name = SDL_GetGamepadName(b->gamepad);
+ if (name) std::strncpy(state->gamepad_name_utf8, name, sizeof(state->gamepad_name_utf8) - 1);
+ return SE_GPU_OK;
+}
+se_gpu_result SE_GPU_CALL se_gpu_present_bgra(
+ se_gpu_backend* b, const void* pixels, size_t size, uint32_t width, uint32_t height, uint32_t pitch) {
+ if (!b || !pixels || !width || !height || pitch < width * 4u || size < static_cast(pitch) * height)
+ return SE_GPU_INVALID_ARGUMENT;
+ if (vkWaitForFences(b->device, 1, &b->frame_fence, VK_TRUE, UINT64_MAX) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "frame fence wait failed");
+ if (b->resized) { vkDeviceWaitIdle(b->device); se_gpu_result recreated = create_swapchain(b);
+ if (recreated != SE_GPU_OK) return recreated; }
+ uint32_t image_index{};
+ VkResult result = vkAcquireNextImageKHR(b->device, b->swapchain, UINT64_MAX,
+ b->image_available, VK_NULL_HANDLE, &image_index);
+ if (result == VK_ERROR_OUT_OF_DATE_KHR) { b->resized = true; return SE_GPU_NOT_READY; }
+ if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR)
+ return fail(b, SE_GPU_VULKAN_ERROR, "swapchain image acquisition failed");
+ uint32_t copy_width = std::min(width, b->extent.width), copy_height = std::min(height, b->extent.height);
+ VkDeviceSize packed_size = static_cast(copy_width) * copy_height * 4u;
+ se_gpu_result staged = ensure_staging(b, packed_size); if (staged != SE_GPU_OK) return staged;
+ const auto* source = static_cast(pixels); auto* destination = static_cast(b->staging_map);
+ bool bgra = b->swapchain_format == VK_FORMAT_B8G8R8A8_UNORM || b->swapchain_format == VK_FORMAT_B8G8R8A8_SRGB;
+ for (uint32_t y = 0; y < copy_height; ++y) {
+ const uint8_t* row = source + static_cast(y) * pitch;
+ uint8_t* output = destination + static_cast(y) * copy_width * 4u;
+ if (bgra) std::memcpy(output, row, static_cast(copy_width) * 4u);
+ else for (uint32_t x = 0; x < copy_width; ++x) {
+ output[x * 4] = row[x * 4 + 2]; output[x * 4 + 1] = row[x * 4 + 1];
+ output[x * 4 + 2] = row[x * 4]; output[x * 4 + 3] = row[x * 4 + 3];
+ }
+ }
+ vkResetFences(b->device, 1, &b->frame_fence); vkResetCommandBuffer(b->command_buffer, 0);
+ VkCommandBufferBeginInfo begin{}; begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+ begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
+ if (vkBeginCommandBuffer(b->command_buffer, &begin) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "command buffer begin failed");
+ VkImageMemoryBarrier transfer{}; transfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
+ transfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; transfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+ transfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; transfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+ transfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; transfer.image = b->swapchain_images[image_index];
+ transfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
+ vkCmdPipelineBarrier(b->command_buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
+ 0, 0, nullptr, 0, nullptr, 1, &transfer);
+ VkBufferImageCopy copy{}; copy.bufferRowLength = copy_width; copy.bufferImageHeight = copy_height;
+ copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; copy.imageExtent = {copy_width, copy_height, 1};
+ vkCmdCopyBufferToImage(b->command_buffer, b->staging, b->swapchain_images[image_index],
+ VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
+ VkImageMemoryBarrier present = transfer; present.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+ present.dstAccessMask = 0; present.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
+ present.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
+ vkCmdPipelineBarrier(b->command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
+ 0, 0, nullptr, 0, nullptr, 1, &present);
+ if (vkEndCommandBuffer(b->command_buffer) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "command buffer end failed");
+ VkPipelineStageFlags wait = VK_PIPELINE_STAGE_TRANSFER_BIT; VkSubmitInfo submit{};
+ submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submit.waitSemaphoreCount = 1;
+ submit.pWaitSemaphores = &b->image_available; submit.pWaitDstStageMask = &wait;
+ submit.commandBufferCount = 1; submit.pCommandBuffers = &b->command_buffer;
+ submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &b->render_finished;
+ if (vkQueueSubmit(b->queue, 1, &submit, b->frame_fence) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "queue submit failed");
+ VkPresentInfoKHR info{}; info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
+ info.waitSemaphoreCount = 1; info.pWaitSemaphores = &b->render_finished;
+ info.swapchainCount = 1; info.pSwapchains = &b->swapchain; info.pImageIndices = &image_index;
+ result = vkQueuePresentKHR(b->queue, &info);
+ if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { b->resized = true; return SE_GPU_OK; }
+ return result == VK_SUCCESS ? SE_GPU_OK : fail(b, SE_GPU_VULKAN_ERROR, "queue present failed");
+}
+se_gpu_result SE_GPU_CALL se_gpu_submit_draw(se_gpu_backend* b, const se_gpu_draw* work) {
+ if (!b || !work || work->struct_size < sizeof(*work) || !work->pixel_spirv.data ||
+ !work->vertex_spirv.data || !work->pixel_spirv.size || !work->vertex_spirv.size ||
+ (work->pixel_spirv.size % 4) != 0 || (work->vertex_spirv.size % 4) != 0 ||
+ !work->width || !work->height || !work->vertex_count || !work->instance_count ||
+ (work->texture_count && !work->textures) || (work->memory_buffer_count && !work->memory_buffers) ||
+ (work->vertex_buffer_count && !work->vertex_buffers) || (work->target_count && !work->targets))
+ return SE_GPU_INVALID_ARGUMENT;
+ std::vector global_buffers(work->memory_buffer_count);
+ std::vector vertex_buffers(work->vertex_buffer_count);
+ host_buffer index_buffer{};
+ std::vector transient_images; transient_images.reserve(work->texture_count + work->target_count + 1);
+ std::vector textures(work->texture_count);
+ std::vector texture_views(work->texture_count);
+ uint32_t target_count = work->target_count ? work->target_count : 1;
+ std::vector targets(target_count);
+ std::vector samplers(work->texture_count);
+ VkDescriptorSetLayout descriptor_layout{}; VkPipelineLayout pipeline_layout{};
+ VkDescriptorPool descriptor_pool{}; VkDescriptorSet descriptor_set{}; VkRenderPass render_pass{};
+ VkFramebuffer framebuffer{}; VkShaderModule vertex_shader{}, pixel_shader{}; VkPipeline pipeline{};
+ auto release = [&] {
+ if (pipeline) vkDestroyPipeline(b->device, pipeline, nullptr);
+ if (pixel_shader) vkDestroyShaderModule(b->device, pixel_shader, nullptr);
+ if (vertex_shader) vkDestroyShaderModule(b->device, vertex_shader, nullptr);
+ if (framebuffer) vkDestroyFramebuffer(b->device, framebuffer, nullptr);
+ if (render_pass) vkDestroyRenderPass(b->device, render_pass, nullptr);
+ if (descriptor_pool) vkDestroyDescriptorPool(b->device, descriptor_pool, nullptr);
+ if (pipeline_layout) vkDestroyPipelineLayout(b->device, pipeline_layout, nullptr);
+ if (descriptor_layout) vkDestroyDescriptorSetLayout(b->device, descriptor_layout, nullptr);
+ for (VkSampler sampler : samplers) if (sampler) vkDestroySampler(b->device, sampler, nullptr);
+ for (uint32_t index = 0; index < work->texture_count; ++index)
+ if (texture_views[index] && texture_views[index] != textures[index]->view)
+ vkDestroyImageView(b->device, texture_views[index], nullptr);
+ destroy_buffer(b, &index_buffer);
+ for (auto& buffer : vertex_buffers) destroy_buffer(b, &buffer);
+ for (auto& buffer : global_buffers) destroy_buffer(b, &buffer);
+ for (auto& image : transient_images) destroy_image(b, &image);
+ };
+ se_gpu_result operation = [&]() -> se_gpu_result {
+ for (uint32_t index = 0; index < work->memory_buffer_count; ++index) {
+ const auto& source = work->memory_buffers[index];
+ se_gpu_result result = create_buffer(b, source.data.data, source.data.size,
+ VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, &global_buffers[index]);
+ if (result != SE_GPU_OK) return result;
+ }
+ for (uint32_t index = 0; index < work->vertex_buffer_count; ++index) {
+ const auto& source = work->vertex_buffers[index];
+ se_gpu_result result = create_buffer(b, source.data.data, source.data.size,
+ VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, &vertex_buffers[index]);
+ if (result != SE_GPU_OK) return result;
+ }
+ if (work->index_buffer) {
+ se_gpu_result result = create_buffer(b, work->index_buffer->data.data,
+ work->index_buffer->data.size, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, &index_buffer);
+ if (result != SE_GPU_OK) return result;
+ }
+ for (uint32_t index = 0; index < work->texture_count; ++index) {
+ const auto& source = work->textures[index]; guest_image* image{};
+ if (source.address) {
+ uint32_t row_length = source.tile_mode == 0 ? std::max(source.pitch, source.width) : source.width;
+ se_gpu_result result = cached_image(b, source.address, source.width, source.height,
+ source.format, source.number_type, row_length, &source.rgba_pixels, &image);
+ if (result != SE_GPU_OK) return result;
+ } else {
+ transient_images.emplace_back(); image = &transient_images.back();
+ se_gpu_result result = create_image(b, source.width, source.height,
+ source.format, source.number_type, image);
+ uint32_t row_length = source.tile_mode == 0 ? std::max(source.pitch, source.width) : source.width;
+ if (result == SE_GPU_OK) result = upload_image(b, image, source.rgba_pixels, row_length);
+ if (result != SE_GPU_OK) return result;
+ }
+ textures[index] = image;
+ se_gpu_result view_result = create_texture_view(b, image, source, &texture_views[index]);
+ if (view_result != SE_GPU_OK) return view_result;
+ }
+ for (uint32_t index = 0; index < target_count; ++index) {
+ guest_image* image{};
+ if (work->target_count && work->targets[index].address) {
+ const auto& source = work->targets[index];
+ se_gpu_result result = cached_image(b, source.address, source.width, source.height,
+ source.format, source.number_type, source.width, nullptr, &image);
+ if (result != SE_GPU_OK) return result;
+ } else {
+ transient_images.emplace_back(); image = &transient_images.back();
+ uint32_t format = work->target_count ? work->targets[index].format : 10;
+ uint32_t number = work->target_count ? work->targets[index].number_type : 0;
+ se_gpu_result result = create_image(b, work->width, work->height, format, number, image);
+ if (result != SE_GPU_OK) return result;
+ }
+ targets[index] = image;
+ }
+ for (uint32_t index = 0; index < work->texture_count; ++index)
+ if (!work->textures[index].is_storage) {
+ se_gpu_result result = create_sampler(b, work->textures[index].sampler, &samplers[index]);
+ if (result != SE_GPU_OK) return result;
+ }
+ std::vector bindings;
+ VkShaderStageFlags resource_stages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
+ if (work->memory_buffer_count) {
+ VkDescriptorSetLayoutBinding binding{}; binding.binding = 0;
+ binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
+ binding.descriptorCount = work->memory_buffer_count; binding.stageFlags = resource_stages;
+ bindings.push_back(binding);
+ }
+ for (uint32_t index = 0; index < work->texture_count; ++index) {
+ VkDescriptorSetLayoutBinding binding{}; binding.binding = index + 1;
+ binding.descriptorType = work->textures[index].is_storage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE :
+ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+ binding.descriptorCount = 1; binding.stageFlags = resource_stages; bindings.push_back(binding);
+ }
+ if (!bindings.empty()) {
+ VkDescriptorSetLayoutCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+ info.bindingCount = static_cast(bindings.size()); info.pBindings = bindings.data();
+ if (vkCreateDescriptorSetLayout(b->device, &info, nullptr, &descriptor_layout) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "graphics descriptor layout creation failed");
+ }
+ VkPipelineLayoutCreateInfo pipeline_layout_info{};
+ pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
+ if (descriptor_layout) { pipeline_layout_info.setLayoutCount = 1; pipeline_layout_info.pSetLayouts = &descriptor_layout; }
+ if (vkCreatePipelineLayout(b->device, &pipeline_layout_info, nullptr, &pipeline_layout) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "graphics pipeline layout creation failed");
+ if (!bindings.empty()) {
+ std::vector sizes;
+ if (work->memory_buffer_count) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, work->memory_buffer_count});
+ uint32_t sampled{}, storage{};
+ for (uint32_t index = 0; index < work->texture_count; ++index)
+ work->textures[index].is_storage ? ++storage : ++sampled;
+ if (sampled) sizes.push_back({VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, sampled});
+ if (storage) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, storage});
+ VkDescriptorPoolCreateInfo pool{}; pool.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
+ pool.maxSets = 1; pool.poolSizeCount = static_cast(sizes.size()); pool.pPoolSizes = sizes.data();
+ if (vkCreateDescriptorPool(b->device, &pool, nullptr, &descriptor_pool) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "graphics descriptor pool creation failed");
+ VkDescriptorSetAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+ allocate.descriptorPool = descriptor_pool; allocate.descriptorSetCount = 1; allocate.pSetLayouts = &descriptor_layout;
+ if (vkAllocateDescriptorSets(b->device, &allocate, &descriptor_set) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "graphics descriptor allocation failed");
+ std::vector buffer_info(work->memory_buffer_count);
+ for (uint32_t index = 0; index < work->memory_buffer_count; ++index)
+ buffer_info[index] = {global_buffers[index].buffer, 0, global_buffers[index].size};
+ std::vector image_info(work->texture_count);
+ std::vector writes;
+ if (work->memory_buffer_count) {
+ VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+ write.dstSet = descriptor_set; write.dstBinding = 0; write.descriptorCount = work->memory_buffer_count;
+ write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; write.pBufferInfo = buffer_info.data();
+ writes.push_back(write);
+ }
+ for (uint32_t index = 0; index < work->texture_count; ++index) {
+ bool storage_image = work->textures[index].is_storage != 0;
+ image_info[index] = {storage_image ? VK_NULL_HANDLE : samplers[index], texture_views[index], VK_IMAGE_LAYOUT_GENERAL};
+ VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+ write.dstSet = descriptor_set; write.dstBinding = index + 1; write.descriptorCount = 1;
+ write.descriptorType = storage_image ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE :
+ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &image_info[index]; writes.push_back(write);
+ }
+ vkUpdateDescriptorSets(b->device, static_cast(writes.size()), writes.data(), 0, nullptr);
+ }
+ std::vector attachments(target_count);
+ std::vector references(target_count);
+ for (uint32_t index = 0; index < target_count; ++index) {
+ attachments[index].format = targets[index]->format; attachments[index].samples = VK_SAMPLE_COUNT_1_BIT;
+ attachments[index].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; attachments[index].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+ attachments[index].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
+ attachments[index].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ attachments[index].initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+ attachments[index].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+ references[index] = {index, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
+ }
+ VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
+ subpass.colorAttachmentCount = target_count; subpass.pColorAttachments = references.data();
+ VkRenderPassCreateInfo pass{}; pass.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
+ pass.attachmentCount = target_count; pass.pAttachments = attachments.data(); pass.subpassCount = 1; pass.pSubpasses = &subpass;
+ if (vkCreateRenderPass(b->device, &pass, nullptr, &render_pass) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "render pass creation failed");
+ std::vector views(target_count);
+ for (uint32_t index = 0; index < target_count; ++index) views[index] = targets[index]->view;
+ VkFramebufferCreateInfo frame{}; frame.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
+ frame.renderPass = render_pass; frame.attachmentCount = target_count; frame.pAttachments = views.data();
+ frame.width = work->width; frame.height = work->height; frame.layers = 1;
+ if (vkCreateFramebuffer(b->device, &frame, nullptr, &framebuffer) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "framebuffer creation failed");
+ VkShaderModuleCreateInfo module{}; module.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
+ module.codeSize = work->vertex_spirv.size; module.pCode = static_cast(work->vertex_spirv.data);
+ if (vkCreateShaderModule(b->device, &module, nullptr, &vertex_shader) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "vertex shader module creation failed");
+ module.codeSize = work->pixel_spirv.size; module.pCode = static_cast(work->pixel_spirv.data);
+ if (vkCreateShaderModule(b->device, &module, nullptr, &pixel_shader) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "pixel shader module creation failed");
+ VkPipelineShaderStageCreateInfo stages[2]{};
+ stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
+ stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vertex_shader; stages[0].pName = "main";
+ stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
+ stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = pixel_shader; stages[1].pName = "main";
+ std::vector vertex_bindings(work->vertex_buffer_count);
+ std::vector attributes(work->vertex_buffer_count);
+ for (uint32_t index = 0; index < work->vertex_buffer_count; ++index) {
+ const auto& source = work->vertex_buffers[index];
+ vertex_bindings[index] = {index, source.stride ? source.stride : std::max(source.component_count, 1u) * 4u,
+ VK_VERTEX_INPUT_RATE_VERTEX};
+ attributes[index] = {source.location, index,
+ vertex_format(source.data_format, source.number_format, source.component_count), 0};
+ }
+ VkPipelineVertexInputStateCreateInfo vertex_input{};
+ vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
+ vertex_input.vertexBindingDescriptionCount = work->vertex_buffer_count;
+ vertex_input.pVertexBindingDescriptions = vertex_bindings.data();
+ vertex_input.vertexAttributeDescriptionCount = work->vertex_buffer_count;
+ vertex_input.pVertexAttributeDescriptions = attributes.data();
+ VkPipelineInputAssemblyStateCreateInfo assembly{};
+ assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
+ assembly.topology = primitive_topology(work->primitive_type);
+ VkPipelineViewportStateCreateInfo viewport_state{};
+ viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
+ viewport_state.viewportCount = 1; viewport_state.scissorCount = 1;
+ VkPipelineRasterizationStateCreateInfo raster{};
+ raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
+ raster.polygonMode = VK_POLYGON_MODE_FILL; raster.cullMode = VK_CULL_MODE_NONE;
+ raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; raster.lineWidth = 1.0f;
+ VkPipelineMultisampleStateCreateInfo multisample{};
+ multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
+ multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
+ std::vector color_blends(target_count);
+ for (uint32_t index = 0; index < target_count; ++index) {
+ const se_gpu_blend* source = work->blend_count ? &work->blends[std::min(index, work->blend_count - 1)] : nullptr;
+ auto& blend = color_blends[index]; blend.colorWriteMask = source ? source->write_mask : 0xf;
+ if (source) { blend.blendEnable = source->enable; blend.srcColorBlendFactor = blend_factor(source->color_src);
+ blend.dstColorBlendFactor = blend_factor(source->color_dst); blend.colorBlendOp = blend_op(source->color_func);
+ blend.srcAlphaBlendFactor = blend_factor(source->separate_alpha ? source->alpha_src : source->color_src);
+ blend.dstAlphaBlendFactor = blend_factor(source->separate_alpha ? source->alpha_dst : source->color_dst);
+ blend.alphaBlendOp = blend_op(source->separate_alpha ? source->alpha_func : source->color_func); }
+ }
+ VkPipelineColorBlendStateCreateInfo blend_state{};
+ blend_state.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
+ blend_state.attachmentCount = target_count; blend_state.pAttachments = color_blends.data();
+ VkDynamicState dynamic_values[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
+ VkPipelineDynamicStateCreateInfo dynamic{}; dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
+ dynamic.dynamicStateCount = 2; dynamic.pDynamicStates = dynamic_values;
+ VkGraphicsPipelineCreateInfo pipeline_info{}; pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
+ pipeline_info.stageCount = 2; pipeline_info.pStages = stages; pipeline_info.pVertexInputState = &vertex_input;
+ pipeline_info.pInputAssemblyState = &assembly; pipeline_info.pViewportState = &viewport_state;
+ pipeline_info.pRasterizationState = &raster; pipeline_info.pMultisampleState = &multisample;
+ pipeline_info.pColorBlendState = &blend_state; pipeline_info.pDynamicState = &dynamic;
+ pipeline_info.layout = pipeline_layout; pipeline_info.renderPass = render_pass;
+ if (vkCreateGraphicsPipelines(b->device, b->pipeline_cache, 1, &pipeline_info, nullptr, &pipeline) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "graphics pipeline creation failed");
+ se_gpu_result begun = begin_immediate(b); if (begun != SE_GPU_OK) return begun;
+ for (guest_image* target : targets) image_barrier(b, target, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
+ VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+ VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
+ VkRenderPassBeginInfo render{}; render.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
+ render.renderPass = render_pass; render.framebuffer = framebuffer;
+ render.renderArea.extent = {work->width, work->height};
+ vkCmdBeginRenderPass(b->command_buffer, &render, VK_SUBPASS_CONTENTS_INLINE);
+ vkCmdBindPipeline(b->command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
+ if (descriptor_set) vkCmdBindDescriptorSets(b->command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
+ pipeline_layout, 0, 1, &descriptor_set, 0, nullptr);
+ if (!vertex_buffers.empty()) {
+ std::vector handles(vertex_buffers.size()); std::vector offsets(vertex_buffers.size());
+ for (size_t index = 0; index < vertex_buffers.size(); ++index) {
+ handles[index] = vertex_buffers[index].buffer;
+ offsets[index] = work->vertex_buffers[index].offset_bytes < vertex_buffers[index].size ?
+ work->vertex_buffers[index].offset_bytes : 0;
+ }
+ vkCmdBindVertexBuffers(b->command_buffer, 0, static_cast(handles.size()), handles.data(), offsets.data());
+ }
+ VkViewport viewport{0, 0, static_cast(work->width), static_cast(work->height), 0, 1};
+ if (work->viewport) viewport = {work->viewport->x, work->viewport->y, work->viewport->width,
+ work->viewport->height, work->viewport->min_depth, work->viewport->max_depth};
+ vkCmdSetViewport(b->command_buffer, 0, 1, &viewport);
+ VkRect2D scissor{{0, 0}, {work->width, work->height}};
+ if (work->scissor) { int32_t x = std::clamp(work->scissor->x, 0, static_cast(work->width));
+ int32_t y = std::clamp(work->scissor->y, 0, static_cast(work->height));
+ scissor.offset = {x, y}; scissor.extent.width = std::min(work->scissor->width, work->width - static_cast(x));
+ scissor.extent.height = std::min(work->scissor->height, work->height - static_cast(y)); }
+ vkCmdSetScissor(b->command_buffer, 0, 1, &scissor);
+ if (work->index_buffer) {
+ vkCmdBindIndexBuffer(b->command_buffer, index_buffer.buffer, 0,
+ work->index_buffer->is_32_bit ? VK_INDEX_TYPE_UINT32 : VK_INDEX_TYPE_UINT16);
+ uint32_t count = static_cast(work->index_buffer->data.size /
+ (work->index_buffer->is_32_bit ? sizeof(uint32_t) : sizeof(uint16_t)));
+ vkCmdDrawIndexed(b->command_buffer, count, work->instance_count, 0, 0, 0);
+ } else {
+ uint32_t count = work->primitive_type == 0x11 ? 4 : work->vertex_count;
+ vkCmdDraw(b->command_buffer, count, work->instance_count, 0, 0);
+ }
+ vkCmdEndRenderPass(b->command_buffer);
+ for (guest_image* target : targets) image_barrier(b, target, VK_IMAGE_LAYOUT_GENERAL,
+ VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT |
+ VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
+ VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
+ return end_immediate(b);
+ }();
+ if (operation == SE_GPU_OK && work->target_count == 0) operation = present_image(b, targets[0]);
+ release(); return operation;
+}
+se_gpu_result SE_GPU_CALL se_gpu_submit_compute(se_gpu_backend* b, const se_gpu_compute* work) {
+ if (!b || !work || work->struct_size < sizeof(*work) || !work->spirv.data ||
+ work->spirv.size == 0 || (work->spirv.size % 4) != 0 ||
+ !work->groups_x || !work->groups_y || !work->groups_z ||
+ (work->texture_count && !work->textures) || (work->memory_buffer_count && !work->memory_buffers))
+ return SE_GPU_INVALID_ARGUMENT;
+ std::vector buffers(work->memory_buffer_count);
+ std::vector transient_images;
+ std::vector images(work->texture_count);
+ std::vector texture_views(work->texture_count);
+ std::vector samplers(work->texture_count);
+ VkDescriptorSetLayout descriptor_layout{}; VkPipelineLayout pipeline_layout{};
+ VkDescriptorPool descriptor_pool{}; VkDescriptorSet descriptor_set{};
+ VkShaderModule shader{}; VkPipeline pipeline{};
+ auto release = [&] {
+ if (pipeline) vkDestroyPipeline(b->device, pipeline, nullptr);
+ if (shader) vkDestroyShaderModule(b->device, shader, nullptr);
+ if (descriptor_pool) vkDestroyDescriptorPool(b->device, descriptor_pool, nullptr);
+ if (pipeline_layout) vkDestroyPipelineLayout(b->device, pipeline_layout, nullptr);
+ if (descriptor_layout) vkDestroyDescriptorSetLayout(b->device, descriptor_layout, nullptr);
+ for (VkSampler sampler : samplers) if (sampler) vkDestroySampler(b->device, sampler, nullptr);
+ for (uint32_t index = 0; index < work->texture_count; ++index)
+ if (texture_views[index] && texture_views[index] != images[index]->view)
+ vkDestroyImageView(b->device, texture_views[index], nullptr);
+ for (auto& image : transient_images) destroy_image(b, &image);
+ for (auto& buffer : buffers) destroy_buffer(b, &buffer);
+ };
+ se_gpu_result operation = [&]() -> se_gpu_result {
+ for (uint32_t index = 0; index < work->memory_buffer_count; ++index) {
+ const auto& source = work->memory_buffers[index];
+ se_gpu_result result = create_buffer(b, source.data.data, source.data.size,
+ VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, &buffers[index]);
+ if (result != SE_GPU_OK) return result;
+ }
+ transient_images.reserve(work->texture_count);
+ for (uint32_t index = 0; index < work->texture_count; ++index) {
+ const auto& source = work->textures[index]; guest_image* image{};
+ if (source.address) {
+ uint32_t row_length = source.tile_mode == 0 ? std::max(source.pitch, source.width) : source.width;
+ se_gpu_result result = cached_image(b, source.address, source.width, source.height,
+ source.format, source.number_type, row_length, &source.rgba_pixels, &image);
+ if (result != SE_GPU_OK) return result;
+ } else {
+ transient_images.emplace_back(); image = &transient_images.back();
+ se_gpu_result result = create_image(b, source.width, source.height,
+ source.format, source.number_type, image);
+ uint32_t row_length = source.tile_mode == 0 ? std::max(source.pitch, source.width) : source.width;
+ if (result == SE_GPU_OK) result = upload_image(b, image, source.rgba_pixels, row_length);
+ if (result != SE_GPU_OK) return result;
+ }
+ images[index] = image;
+ se_gpu_result view_result = create_texture_view(b, image, source, &texture_views[index]);
+ if (view_result != SE_GPU_OK) return view_result;
+ }
+ for (uint32_t index = 0; index < work->texture_count; ++index)
+ if (!work->textures[index].is_storage) {
+ se_gpu_result result = create_sampler(b, work->textures[index].sampler, &samplers[index]);
+ if (result != SE_GPU_OK) return result;
+ }
+ std::vector bindings;
+ if (work->memory_buffer_count) {
+ VkDescriptorSetLayoutBinding binding{}; binding.binding = 0;
+ binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
+ binding.descriptorCount = work->memory_buffer_count; binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
+ bindings.push_back(binding);
+ }
+ for (uint32_t index = 0; index < work->texture_count; ++index) {
+ VkDescriptorSetLayoutBinding binding{}; binding.binding = index + 1;
+ binding.descriptorType = work->textures[index].is_storage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE :
+ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+ binding.descriptorCount = 1; binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; bindings.push_back(binding);
+ }
+ if (!bindings.empty()) {
+ VkDescriptorSetLayoutCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+ info.bindingCount = static_cast(bindings.size()); info.pBindings = bindings.data();
+ if (vkCreateDescriptorSetLayout(b->device, &info, nullptr, &descriptor_layout) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "compute descriptor layout creation failed");
+ }
+ VkPipelineLayoutCreateInfo pipeline_layout_info{};
+ pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
+ if (descriptor_layout) { pipeline_layout_info.setLayoutCount = 1; pipeline_layout_info.pSetLayouts = &descriptor_layout; }
+ if (vkCreatePipelineLayout(b->device, &pipeline_layout_info, nullptr, &pipeline_layout) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "compute pipeline layout creation failed");
+ if (!bindings.empty()) {
+ std::vector sizes;
+ if (work->memory_buffer_count) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, work->memory_buffer_count});
+ uint32_t sampled{}, storage{};
+ for (uint32_t index = 0; index < work->texture_count; ++index)
+ work->textures[index].is_storage ? ++storage : ++sampled;
+ if (sampled) sizes.push_back({VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, sampled});
+ if (storage) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, storage});
+ VkDescriptorPoolCreateInfo pool{}; pool.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
+ pool.maxSets = 1; pool.poolSizeCount = static_cast(sizes.size()); pool.pPoolSizes = sizes.data();
+ if (vkCreateDescriptorPool(b->device, &pool, nullptr, &descriptor_pool) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "compute descriptor pool creation failed");
+ VkDescriptorSetAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+ allocate.descriptorPool = descriptor_pool; allocate.descriptorSetCount = 1;
+ allocate.pSetLayouts = &descriptor_layout;
+ if (vkAllocateDescriptorSets(b->device, &allocate, &descriptor_set) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "compute descriptor allocation failed");
+ std::vector buffer_info(work->memory_buffer_count);
+ for (uint32_t index = 0; index < work->memory_buffer_count; ++index)
+ buffer_info[index] = {buffers[index].buffer, 0, buffers[index].size};
+ std::vector image_info(work->texture_count);
+ std::vector writes;
+ if (work->memory_buffer_count) {
+ VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+ write.dstSet = descriptor_set; write.dstBinding = 0;
+ write.descriptorCount = work->memory_buffer_count; write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
+ write.pBufferInfo = buffer_info.data(); writes.push_back(write);
+ }
+ for (uint32_t index = 0; index < work->texture_count; ++index) {
+ bool storage_image = work->textures[index].is_storage != 0;
+ image_info[index] = {storage_image ? VK_NULL_HANDLE : samplers[index], texture_views[index],
+ VK_IMAGE_LAYOUT_GENERAL};
+ VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+ write.dstSet = descriptor_set; write.dstBinding = index + 1; write.descriptorCount = 1;
+ write.descriptorType = storage_image ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE :
+ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &image_info[index];
+ writes.push_back(write);
+ }
+ vkUpdateDescriptorSets(b->device, static_cast(writes.size()), writes.data(), 0, nullptr);
+ }
+ VkShaderModuleCreateInfo shader_info{}; shader_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
+ shader_info.codeSize = work->spirv.size; shader_info.pCode = static_cast(work->spirv.data);
+ if (vkCreateShaderModule(b->device, &shader_info, nullptr, &shader) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "compute shader module creation failed");
+ VkComputePipelineCreateInfo pipeline_info{}; pipeline_info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
+ pipeline_info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
+ pipeline_info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; pipeline_info.stage.module = shader;
+ pipeline_info.stage.pName = "main"; pipeline_info.layout = pipeline_layout;
+ if (vkCreateComputePipelines(b->device, b->pipeline_cache, 1, &pipeline_info, nullptr, &pipeline) != VK_SUCCESS)
+ return fail(b, SE_GPU_VULKAN_ERROR, "compute pipeline creation failed");
+ se_gpu_result begun = begin_immediate(b); if (begun != SE_GPU_OK) return begun;
+ vkCmdBindPipeline(b->command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
+ if (descriptor_set) vkCmdBindDescriptorSets(b->command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE,
+ pipeline_layout, 0, 1, &descriptor_set, 0, nullptr);
+ vkCmdDispatch(b->command_buffer, work->groups_x, work->groups_y, work->groups_z);
+ VkMemoryBarrier barrier{}; barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
+ barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
+ barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT;
+ vkCmdPipelineBarrier(b->command_buffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
+ return end_immediate(b);
+ }();
+ release(); return operation;
+}
+se_gpu_result SE_GPU_CALL se_gpu_register_display_buffer(se_gpu_backend* b, uint64_t address, uint32_t format) {
+ if (!b || !address) return SE_GPU_INVALID_ARGUMENT;
+ b->display_formats[address] = format; return SE_GPU_OK;
+}
+se_gpu_result SE_GPU_CALL se_gpu_present_guest_image(
+ se_gpu_backend* b, uint64_t address, uint32_t width, uint32_t height, uint32_t pitch) {
+ if (!b || !address || !width || !height || pitch < width) return SE_GPU_INVALID_ARGUMENT;
+ auto found = b->guest_images.find(address); if (found == b->guest_images.end()) return SE_GPU_NOT_FOUND;
+ return present_image(b, &found->second);
+}
+se_gpu_result SE_GPU_CALL se_gpu_has_guest_image(
+ se_gpu_backend* b, uint64_t address, uint32_t format, uint32_t number_type) {
+ if (!b || !address) return SE_GPU_INVALID_ARGUMENT;
+ auto found = b->guest_images.find(address); if (found == b->guest_images.end()) return SE_GPU_NOT_FOUND;
+ VkFormat expected = guest_format(format, number_type);
+ return expected == VK_FORMAT_UNDEFINED || found->second.format == expected ? SE_GPU_OK : SE_GPU_NOT_FOUND;
+}
+se_gpu_result SE_GPU_CALL se_gpu_blit_guest_image(
+ se_gpu_backend* b, uint64_t source_address, uint32_t source_width, uint32_t source_height,
+ uint32_t source_format, uint64_t destination_address, uint32_t destination_width,
+ uint32_t destination_height, uint32_t destination_format) {
+ if (!b || !source_address || !destination_address || !source_width || !source_height ||
+ !destination_width || !destination_height) return SE_GPU_INVALID_ARGUMENT;
+ auto source = b->guest_images.find(source_address);
+ if (source == b->guest_images.end()) return SE_GPU_NOT_FOUND;
+ if (guest_format(source_format, 0) != VK_FORMAT_UNDEFINED &&
+ source->second.format != guest_format(source_format, 0)) return SE_GPU_NOT_FOUND;
+ guest_image* destination{};
+ se_gpu_result created = cached_image(b, destination_address, destination_width, destination_height,
+ destination_format, 0, destination_width, nullptr, &destination);
+ if (created != SE_GPU_OK) return created;
+ source = b->guest_images.find(source_address);
+ return source == b->guest_images.end() ? SE_GPU_NOT_FOUND : blit_image(b, &source->second, destination);
+}
+se_gpu_result SE_GPU_CALL se_gpu_render_target_output_kind(
+ uint32_t format, uint32_t number_type, uint32_t* output_kind) {
+ if (!output_kind) return SE_GPU_INVALID_ARGUMENT;
+ bool supported = false; uint32_t kind = 0;
+ if ((format == 4 || format == 5 || format == 10 || format == 12) && number_type == 4) {
+ supported = true; kind = 1;
+ } else if ((format == 4 || format == 5 || format == 10 || format == 12) && number_type == 5) {
+ supported = true; kind = 2;
+ } else {
+ switch (format) {
+ case 1: case 3: case 4: case 5: case 7: case 9: case 10: case 12: case 13: case 14:
+ case 22: case 29: case 36: case 56: case 62: case 64: case 71: case 75:
+ case 0x30004: case 0x30005:
+ supported = true; kind = 0; break;
+ case 49: case 0x10004: case 0x10005: case 0x1000a: case 0x1000c:
+ supported = true; kind = 1; break;
+ case 0x20004: case 0x20005: case 0x2000a: case 0x2000c:
+ supported = true; kind = 2; break;
+ default: break;
+ }
+ }
+ if (!supported) return SE_GPU_NOT_FOUND;
+ *output_kind = kind; return SE_GPU_OK;
+}
+}
diff --git a/src/SharpEmu.Gpu.Vulkan.Native/tests/abi_test.cpp b/src/SharpEmu.Gpu.Vulkan.Native/tests/abi_test.cpp
new file mode 100644
index 0000000..1392bae
--- /dev/null
+++ b/src/SharpEmu.Gpu.Vulkan.Native/tests/abi_test.cpp
@@ -0,0 +1,55 @@
+#include "sharpemu_gpu_vulkan.h"
+#include
+#include
+#include
+#include
+#include
+
+std::vector read_file(const char* path) {
+ std::ifstream file(path, std::ios::binary);
+ return {std::istreambuf_iterator(file), std::istreambuf_iterator()};
+}
+
+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 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 vertex = read_file(argv[2]);
+ std::vector 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 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 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;
+}
diff --git a/src/SharpEmu.Gpu.Vulkan.Native/tests/exports_test.cpp b/src/SharpEmu.Gpu.Vulkan.Native/tests/exports_test.cpp
new file mode 100644
index 0000000..4607894
--- /dev/null
+++ b/src/SharpEmu.Gpu.Vulkan.Native/tests/exports_test.cpp
@@ -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;
+}
diff --git a/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.comp b/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.comp
new file mode 100644
index 0000000..f1abb57
--- /dev/null
+++ b/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.comp
@@ -0,0 +1,3 @@
+#version 450
+layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
+void main() {}
diff --git a/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.frag b/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.frag
new file mode 100644
index 0000000..091a058
--- /dev/null
+++ b/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.frag
@@ -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)); }
diff --git a/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.vert b/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.vert
new file mode 100644
index 0000000..c183949
--- /dev/null
+++ b/src/SharpEmu.Gpu.Vulkan.Native/tests/smoke.vert
@@ -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); }
diff --git a/src/SharpEmu.Libs/Gpu/GuestGpu.cs b/src/SharpEmu.Libs/Gpu/GuestGpu.cs
index 78c6b1f..24dc420 100644
--- a/src/SharpEmu.Libs/Gpu/GuestGpu.cs
+++ b/src/SharpEmu.Libs/Gpu/GuestGpu.cs
@@ -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;
///
internal static class GuestGpu
{
- private static readonly Lazy Instance = new(static () => new VulkanGuestGpuBackend());
+ private static readonly Lazy Instance = new(static () =>
+ string.Equals(
+ Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND"),
+ "native",
+ StringComparison.OrdinalIgnoreCase)
+ ? new NativeVulkanGuestGpuBackend()
+ : new VulkanGuestGpuBackend());
public static IGuestGpuBackend Current => Instance.Value;
}
diff --git a/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeGpuInputSource.cs b/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeGpuInputSource.cs
new file mode 100644
index 0000000..953f41c
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeGpuInputSource.cs
@@ -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 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;
+ }
+}
diff --git a/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeGpuPacket.cs b/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeGpuPacket.cs
new file mode 100644
index 0000000..ccc8b12
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeGpuPacket.cs
@@ -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 textures,
+ IReadOnlyList memoryBuffers,
+ uint width,
+ uint height,
+ uint attributeCount,
+ VulkanCompiledGuestShader? vertexShader,
+ uint vertexCount,
+ uint instanceCount,
+ uint primitiveType,
+ GuestIndexBuffer? indexBuffer,
+ IReadOnlyList? vertexBuffers,
+ GuestRenderState? renderState,
+ IReadOnlyList? targets,
+ bool publishTargets)
+ {
+ using var storage = new Storage();
+ var nativeTextures = storage.Allocate(textures.Count);
+ for (var index = 0; index < textures.Count; ++index)
+ nativeTextures[index] = Texture(textures[index], storage);
+ var nativeMemory = storage.Allocate(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(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(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(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 textures,
+ IReadOnlyList buffers,
+ uint x, uint y, uint z)
+ {
+ using var storage = new Storage();
+ var nativeTextures = storage.Allocate(textures.Count);
+ for (var index = 0; index < textures.Count; ++index) nativeTextures[index] = Texture(textures[index], storage);
+ var nativeBuffers = storage.Allocate(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 _pins = [];
+ private readonly List _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(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);
+ }
+ }
+}
diff --git a/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeVulkanApi.cs b/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeVulkanApi.cs
new file mode 100644
index 0000000..1998e40
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeVulkanApi.cs
@@ -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] 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)!;
+ }
+}
diff --git a/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeVulkanGuestGpuBackend.cs b/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeVulkanGuestGpuBackend.cs
new file mode 100644
index 0000000..8501f2d
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/NativeVulkan/NativeVulkanGuestGpuBackend.cs
@@ -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;
+
+/// Native C++ Vulkan implementation of the guest-domain GPU seam.
+internal sealed unsafe class NativeVulkanGuestGpuBackend : IGuestGpuBackend
+{
+ private readonly object _startGate = new();
+ private readonly BlockingCollection> _commands = new(new ConcurrentQueue>(), 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 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 textures,
+ IReadOnlyList globalMemoryBuffers, uint width, uint height, uint attributeCount,
+ IGuestCompiledShader? vertexShader = null, uint vertexCount = 3, uint instanceCount = 1,
+ uint primitiveType = 4, GuestIndexBuffer? indexBuffer = null,
+ IReadOnlyList? 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 textures, IReadOnlyList globalMemoryBuffers,
+ uint attributeCount, IReadOnlyList targets, IGuestCompiledShader? vertexShader = null,
+ uint vertexCount = 3, uint instanceCount = 1, uint primitiveType = 4,
+ GuestIndexBuffer? indexBuffer = null, IReadOnlyList? 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 textures, IReadOnlyList 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 textures, IReadOnlyList 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 command)
+ {
+ if (!_commands.TryAdd(command)) Console.Error.WriteLine("[LOADER][WARN] Native GPU queue is full; dropping work");
+ }
+
+ private NativeGpuResult Invoke(Func operation)
+ {
+ var completion = new TaskCompletionSource(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");
+}
diff --git a/src/SharpEmu.Libs/SharpEmu.Libs.csproj b/src/SharpEmu.Libs/SharpEmu.Libs.csproj
index 9b0babf..b7e3d04 100644
--- a/src/SharpEmu.Libs/SharpEmu.Libs.csproj
+++ b/src/SharpEmu.Libs/SharpEmu.Libs.csproj
@@ -37,5 +37,43 @@ SPDX-License-Identifier: GPL-2.0-or-later
true
+ $(MSBuildThisFileDirectory)..\SharpEmu.Gpu.Vulkan.Native
+ $(RepoRoot)artifacts\native\gpu-vulkan
+
+
+ $(NativeGpuBuildDir)\bin\sharpemu_gpu_vulkan.dll
+ sharpemu_gpu_vulkan.dll
+ $(NativeGpuBuildDir)\bin\SDL3.dll
+
+
+ $(NativeGpuBuildDir)\bin\libsharpemu_gpu_vulkan.so
+ libsharpemu_gpu_vulkan.so
+
+
+ $(NativeGpuBuildDir)\bin\libsharpemu_gpu_vulkan.dylib
+ libsharpemu_gpu_vulkan.dylib
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs
index 479a27e..51237a0 100644
--- a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs
@@ -5,6 +5,36 @@ namespace SharpEmu.ShaderCompiler.Vulkan;
public static class SpirvFixedShaders
{
+ /// Fragment half of the fixed fullscreen barycentric diagnostic draw.
+ 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();