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();