mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-31 22:00:48 +08:00
Rewrite Vulkan backend with native renderer
This commit is contained in:
@@ -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
|
||||||
@@ -96,6 +96,19 @@ jobs:
|
|||||||
Directory.Packages.props
|
Directory.Packages.props
|
||||||
Directory.Build.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
|
- name: Restore solution
|
||||||
run: dotnet restore SharpEmu.slnx
|
run: dotnet restore SharpEmu.slnx
|
||||||
|
|
||||||
@@ -114,6 +127,19 @@ jobs:
|
|||||||
- name: Publish win-x64 CLI
|
- 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}"
|
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
|
- name: Create release archive
|
||||||
run: |
|
run: |
|
||||||
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public sealed class GuiSettings
|
|||||||
|
|
||||||
public bool StrictDynlibResolution { get; set; }
|
public bool StrictDynlibResolution { get; set; }
|
||||||
|
|
||||||
|
/// <summary>GPU implementation selected for newly launched games.</summary>
|
||||||
|
public string RenderingBackend { get; set; } = "Legacy";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -43,6 +43,11 @@
|
|||||||
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||||
"Options.CpuEngine.Native": "Native",
|
"Options.CpuEngine.Native": "Native",
|
||||||
|
|
||||||
|
"Options.RenderingBackend.Label": "Rendering backend",
|
||||||
|
"Options.RenderingBackend.Desc": "Graphics implementation used for newly launched games.",
|
||||||
|
"Options.RenderingBackend.Native": "Native Vulkan",
|
||||||
|
"Options.RenderingBackend.Legacy": "Silk.NET Vulkan (Legacy)",
|
||||||
|
|
||||||
"Options.Strict.Label": "Strict dynlib resolution",
|
"Options.Strict.Label": "Strict dynlib resolution",
|
||||||
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,19 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</ComboBox>
|
</ComboBox>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
|
<TextBlock x:Name="RenderingBackendLabel" Text="Rendering backend" FontSize="13" />
|
||||||
|
<TextBlock x:Name="RenderingBackendDesc" Text="Graphics implementation used for newly launched games."
|
||||||
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ComboBox Grid.Column="1" x:Name="RenderingBackendBox" Width="200" SelectedIndex="0"
|
||||||
|
VerticalAlignment="Center" CornerRadius="8">
|
||||||
|
<ComboBoxItem x:Name="RenderingBackendLegacyItem" Content="Silk.NET Vulkan (Legacy)" />
|
||||||
|
<ComboBoxItem x:Name="RenderingBackendNativeItem" Content="Native Vulkan" />
|
||||||
|
</ComboBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ public partial class MainWindow : Window
|
|||||||
// The settings page edits _settings live, so a launch started while
|
// The settings page edits _settings live, so a launch started while
|
||||||
// it is open already uses the new values.
|
// it is open already uses the new values.
|
||||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||||
|
RenderingBackendBox.SelectionChanged += (_, _) =>
|
||||||
|
_settings.RenderingBackend = SelectedRenderingBackend();
|
||||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||||
@@ -439,6 +441,11 @@ public partial class MainWindow : Window
|
|||||||
CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
|
CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
|
||||||
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
||||||
|
|
||||||
|
RenderingBackendLabel.Text = loc.Get("Options.RenderingBackend.Label");
|
||||||
|
RenderingBackendDesc.Text = loc.Get("Options.RenderingBackend.Desc");
|
||||||
|
RenderingBackendNativeItem.Content = loc.Get("Options.RenderingBackend.Native");
|
||||||
|
RenderingBackendLegacyItem.Content = loc.Get("Options.RenderingBackend.Legacy");
|
||||||
|
|
||||||
StrictLabel.Text = loc.Get("Options.Strict.Label");
|
StrictLabel.Text = loc.Get("Options.Strict.Label");
|
||||||
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
||||||
|
|
||||||
@@ -596,6 +603,10 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void ApplySettingsToControls()
|
private void ApplySettingsToControls()
|
||||||
{
|
{
|
||||||
|
RenderingBackendBox.SelectedIndex = string.Equals(
|
||||||
|
_settings.RenderingBackend,
|
||||||
|
"Native",
|
||||||
|
StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||||
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"trace" => 0,
|
"trace" => 0,
|
||||||
@@ -654,6 +665,9 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string SelectedRenderingBackend() =>
|
||||||
|
RenderingBackendBox.SelectedIndex == 1 ? "Native" : "Legacy";
|
||||||
|
|
||||||
private void UpdateLogFilePathText()
|
private void UpdateLogFilePathText()
|
||||||
{
|
{
|
||||||
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
||||||
@@ -1473,6 +1487,15 @@ public partial class MainWindow : Window
|
|||||||
_appliedEnvironmentVariables.Add(name);
|
_appliedEnvironmentVariables.Add(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The GUI owns this setting when it launches the emulator. Set both
|
||||||
|
// choices explicitly so a SHARPEMU_GPU_BACKEND value inherited by the
|
||||||
|
// launcher cannot silently override the Options menu.
|
||||||
|
Environment.SetEnvironmentVariable(
|
||||||
|
"SHARPEMU_GPU_BACKEND",
|
||||||
|
string.Equals(_settings.RenderingBackend, "Legacy", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "legacy"
|
||||||
|
: "native");
|
||||||
|
|
||||||
var emulator = new EmulatorProcess();
|
var emulator = new EmulatorProcess();
|
||||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.25)
|
||||||
|
project(SharpEmuGpuVulkan VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
find_package(Vulkan REQUIRED)
|
||||||
|
find_package(SDL3 CONFIG QUIET)
|
||||||
|
if(TARGET SDL3::SDL3)
|
||||||
|
set(SE_GPU_SDL_TARGET SDL3::SDL3)
|
||||||
|
elseif(TARGET SDL3::SDL3-shared)
|
||||||
|
set(SE_GPU_SDL_TARGET SDL3::SDL3-shared)
|
||||||
|
else()
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3>=3.2)
|
||||||
|
set(SE_GPU_SDL_TARGET PkgConfig::SDL3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(sharpemu_gpu_vulkan SHARED src/backend.cpp)
|
||||||
|
target_compile_features(sharpemu_gpu_vulkan PRIVATE cxx_std_20)
|
||||||
|
target_compile_definitions(sharpemu_gpu_vulkan PRIVATE SE_GPU_BUILD)
|
||||||
|
target_include_directories(sharpemu_gpu_vulkan PUBLIC include)
|
||||||
|
target_link_libraries(sharpemu_gpu_vulkan PRIVATE Vulkan::Vulkan ${SE_GPU_SDL_TARGET})
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(sharpemu_gpu_vulkan PRIVATE /W4 /permissive- /EHsc)
|
||||||
|
else()
|
||||||
|
target_compile_options(sharpemu_gpu_vulkan PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Werror)
|
||||||
|
endif()
|
||||||
|
set_target_properties(sharpemu_gpu_vulkan PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden
|
||||||
|
VISIBILITY_INLINES_HIDDEN YES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
||||||
|
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||||
|
|
||||||
|
# Visual Studio is a multi-config generator and otherwise inserts an extra
|
||||||
|
# Debug/Release directory. Keep the ABI library at the path consumed by the
|
||||||
|
# managed project on every generator.
|
||||||
|
foreach(SE_GPU_CONFIG Debug Release RelWithDebInfo MinSizeRel)
|
||||||
|
string(TOUPPER "${SE_GPU_CONFIG}" SE_GPU_CONFIG_UPPER)
|
||||||
|
set_target_properties(sharpemu_gpu_vulkan PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_${SE_GPU_CONFIG_UPPER} "${CMAKE_BINARY_DIR}/bin"
|
||||||
|
LIBRARY_OUTPUT_DIRECTORY_${SE_GPU_CONFIG_UPPER} "${CMAKE_BINARY_DIR}/bin")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
add_custom_command(TARGET sharpemu_gpu_vulkan POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
$<TARGET_RUNTIME_DLLS:sharpemu_gpu_vulkan>
|
||||||
|
$<TARGET_FILE_DIR:sharpemu_gpu_vulkan>
|
||||||
|
COMMAND_EXPAND_LISTS)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_executable(sharpemu_gpu_vulkan_exports_test tests/exports_test.cpp)
|
||||||
|
target_link_libraries(sharpemu_gpu_vulkan_exports_test PRIVATE sharpemu_gpu_vulkan)
|
||||||
|
set_target_properties(sharpemu_gpu_vulkan_exports_test PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||||
|
foreach(SE_GPU_CONFIG Debug Release RelWithDebInfo MinSizeRel)
|
||||||
|
string(TOUPPER "${SE_GPU_CONFIG}" SE_GPU_CONFIG_UPPER)
|
||||||
|
set_target_properties(sharpemu_gpu_vulkan_exports_test PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_${SE_GPU_CONFIG_UPPER} "${CMAKE_BINARY_DIR}/bin")
|
||||||
|
endforeach()
|
||||||
|
add_test(NAME sharpemu_gpu_vulkan_exports_test COMMAND sharpemu_gpu_vulkan_exports_test)
|
||||||
|
|
||||||
|
if(TARGET Vulkan::glslc)
|
||||||
|
set(SMOKE_COMPUTE_SPV "${CMAKE_CURRENT_BINARY_DIR}/smoke.comp.spv")
|
||||||
|
set(SMOKE_VERTEX_SPV "${CMAKE_CURRENT_BINARY_DIR}/smoke.vert.spv")
|
||||||
|
set(SMOKE_FRAGMENT_SPV "${CMAKE_CURRENT_BINARY_DIR}/smoke.frag.spv")
|
||||||
|
add_custom_command(OUTPUT "${SMOKE_COMPUTE_SPV}"
|
||||||
|
COMMAND Vulkan::glslc "${CMAKE_CURRENT_SOURCE_DIR}/tests/smoke.comp" -o "${SMOKE_COMPUTE_SPV}"
|
||||||
|
DEPENDS tests/smoke.comp VERBATIM)
|
||||||
|
add_custom_command(OUTPUT "${SMOKE_VERTEX_SPV}"
|
||||||
|
COMMAND Vulkan::glslc "${CMAKE_CURRENT_SOURCE_DIR}/tests/smoke.vert" -o "${SMOKE_VERTEX_SPV}"
|
||||||
|
DEPENDS tests/smoke.vert VERBATIM)
|
||||||
|
add_custom_command(OUTPUT "${SMOKE_FRAGMENT_SPV}"
|
||||||
|
COMMAND Vulkan::glslc "${CMAKE_CURRENT_SOURCE_DIR}/tests/smoke.frag" -o "${SMOKE_FRAGMENT_SPV}"
|
||||||
|
DEPENDS tests/smoke.frag VERBATIM)
|
||||||
|
add_custom_target(sharpemu_gpu_vulkan_test_shaders
|
||||||
|
DEPENDS "${SMOKE_COMPUTE_SPV}" "${SMOKE_VERTEX_SPV}" "${SMOKE_FRAGMENT_SPV}")
|
||||||
|
add_executable(sharpemu_gpu_vulkan_abi_test tests/abi_test.cpp)
|
||||||
|
target_link_libraries(sharpemu_gpu_vulkan_abi_test PRIVATE sharpemu_gpu_vulkan)
|
||||||
|
add_dependencies(sharpemu_gpu_vulkan_abi_test sharpemu_gpu_vulkan_test_shaders)
|
||||||
|
add_test(NAME sharpemu_gpu_vulkan_abi_test
|
||||||
|
COMMAND sharpemu_gpu_vulkan_abi_test
|
||||||
|
"${SMOKE_COMPUTE_SPV}" "${SMOKE_VERTEX_SPV}" "${SMOKE_FRAGMENT_SPV}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/* Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
# define SE_GPU_CALL __cdecl
|
||||||
|
# if defined(SE_GPU_BUILD)
|
||||||
|
# define SE_GPU_API __declspec(dllexport)
|
||||||
|
# else
|
||||||
|
# define SE_GPU_API __declspec(dllimport)
|
||||||
|
# endif
|
||||||
|
#else
|
||||||
|
# define SE_GPU_CALL
|
||||||
|
# define SE_GPU_API __attribute__((visibility("default")))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SE_GPU_ABI_VERSION 1u
|
||||||
|
typedef struct se_gpu_backend se_gpu_backend;
|
||||||
|
typedef enum se_gpu_result {
|
||||||
|
SE_GPU_OK = 0, SE_GPU_NOT_FOUND = 1, SE_GPU_NOT_READY = 2,
|
||||||
|
SE_GPU_INVALID_ARGUMENT = -1, SE_GPU_INCOMPATIBLE_ABI = -2,
|
||||||
|
SE_GPU_PLATFORM_ERROR = -3, SE_GPU_VULKAN_ERROR = -4,
|
||||||
|
SE_GPU_OUT_OF_MEMORY = -5, SE_GPU_INTERNAL_ERROR = -6
|
||||||
|
} se_gpu_result;
|
||||||
|
typedef struct se_gpu_bytes { const void* data; size_t size; } se_gpu_bytes;
|
||||||
|
typedef void (SE_GPU_CALL *se_gpu_log_fn)(int32_t level, const char* message, void* user);
|
||||||
|
typedef struct se_gpu_create_info {
|
||||||
|
uint32_t struct_size, abi_version, width, height, enable_validation;
|
||||||
|
const char* title_utf8;
|
||||||
|
se_gpu_log_fn log; void* log_user;
|
||||||
|
} se_gpu_create_info;
|
||||||
|
typedef struct se_gpu_sampler { uint32_t words[4]; } se_gpu_sampler;
|
||||||
|
typedef struct se_gpu_texture {
|
||||||
|
uint32_t struct_size; uint64_t address; uint32_t width, height, format, number_type;
|
||||||
|
se_gpu_bytes rgba_pixels; uint32_t is_fallback, is_storage, mip_levels, mip_level;
|
||||||
|
uint32_t pitch, tile_mode, dst_select; se_gpu_sampler sampler;
|
||||||
|
} se_gpu_texture;
|
||||||
|
typedef struct se_gpu_memory_buffer { uint64_t address; se_gpu_bytes data; } se_gpu_memory_buffer;
|
||||||
|
typedef struct se_gpu_vertex_buffer {
|
||||||
|
uint32_t struct_size, location, component_count, data_format, number_format;
|
||||||
|
uint64_t address; uint32_t stride, offset_bytes; se_gpu_bytes data;
|
||||||
|
} se_gpu_vertex_buffer;
|
||||||
|
typedef struct se_gpu_index_buffer { se_gpu_bytes data; uint32_t is_32_bit; } se_gpu_index_buffer;
|
||||||
|
typedef struct se_gpu_rect { int32_t x, y; uint32_t width, height; } se_gpu_rect;
|
||||||
|
typedef struct se_gpu_viewport { float x, y, width, height, min_depth, max_depth; } se_gpu_viewport;
|
||||||
|
typedef struct se_gpu_blend {
|
||||||
|
uint32_t enable, color_src, color_dst, color_func, alpha_src, alpha_dst, alpha_func;
|
||||||
|
uint32_t separate_alpha, write_mask;
|
||||||
|
} se_gpu_blend;
|
||||||
|
typedef struct se_gpu_render_target {
|
||||||
|
uint32_t struct_size; uint64_t address; uint32_t width, height, format, number_type, mip_levels;
|
||||||
|
} se_gpu_render_target;
|
||||||
|
typedef struct se_gpu_draw {
|
||||||
|
uint32_t struct_size, width, height; se_gpu_bytes vertex_spirv, pixel_spirv;
|
||||||
|
const se_gpu_texture* textures; uint32_t texture_count;
|
||||||
|
const se_gpu_memory_buffer* memory_buffers; uint32_t memory_buffer_count;
|
||||||
|
const se_gpu_vertex_buffer* vertex_buffers; uint32_t vertex_buffer_count;
|
||||||
|
const se_gpu_render_target* targets; uint32_t target_count;
|
||||||
|
const se_gpu_blend* blends; uint32_t blend_count;
|
||||||
|
const se_gpu_index_buffer* index_buffer; const se_gpu_rect* scissor;
|
||||||
|
const se_gpu_viewport* viewport; uint32_t attribute_count, vertex_count, instance_count;
|
||||||
|
uint32_t primitive_type, publish_targets;
|
||||||
|
} se_gpu_draw;
|
||||||
|
typedef struct se_gpu_compute {
|
||||||
|
uint32_t struct_size; uint64_t shader_address; se_gpu_bytes spirv;
|
||||||
|
const se_gpu_texture* textures; uint32_t texture_count;
|
||||||
|
const se_gpu_memory_buffer* memory_buffers; uint32_t memory_buffer_count;
|
||||||
|
uint32_t groups_x, groups_y, groups_z;
|
||||||
|
} se_gpu_compute;
|
||||||
|
typedef struct se_gpu_input {
|
||||||
|
uint32_t struct_size, keyboard_focused, virtual_keys[8], gamepad_connected, gamepad_buttons;
|
||||||
|
uint8_t left_x, left_y, right_x, right_y, left_trigger, right_trigger, reserved[2];
|
||||||
|
char gamepad_name_utf8[128];
|
||||||
|
} se_gpu_input;
|
||||||
|
|
||||||
|
SE_GPU_API uint32_t SE_GPU_CALL se_gpu_abi_version(void);
|
||||||
|
SE_GPU_API const char* SE_GPU_CALL se_gpu_last_error(const se_gpu_backend* backend);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_create(const se_gpu_create_info*, se_gpu_backend**);
|
||||||
|
SE_GPU_API void SE_GPU_CALL se_gpu_destroy(se_gpu_backend*);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_poll(se_gpu_backend*, uint32_t* should_close);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_input_snapshot(se_gpu_backend*, se_gpu_input*);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_present_bgra(
|
||||||
|
se_gpu_backend*, const void* pixels, size_t size, uint32_t width, uint32_t height, uint32_t pitch);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_submit_draw(se_gpu_backend*, const se_gpu_draw*);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_submit_compute(se_gpu_backend*, const se_gpu_compute*);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_register_display_buffer(se_gpu_backend*, uint64_t, uint32_t);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_present_guest_image(
|
||||||
|
se_gpu_backend*, uint64_t, uint32_t, uint32_t, uint32_t);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_has_guest_image(
|
||||||
|
se_gpu_backend*, uint64_t, uint32_t, uint32_t);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_blit_guest_image(
|
||||||
|
se_gpu_backend*, uint64_t, uint32_t, uint32_t, uint32_t,
|
||||||
|
uint64_t, uint32_t, uint32_t, uint32_t);
|
||||||
|
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_render_target_output_kind(
|
||||||
|
uint32_t format, uint32_t number_type, uint32_t* output_kind);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
|||||||
|
#include "sharpemu_gpu_vulkan.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
std::vector<char> read_file(const char* path) {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
if (se_gpu_abi_version() != SE_GPU_ABI_VERSION) return 1;
|
||||||
|
uint32_t output_kind{};
|
||||||
|
if (se_gpu_render_target_output_kind(10, 4, &output_kind) != SE_GPU_OK || output_kind != 1) return 5;
|
||||||
|
if (se_gpu_render_target_output_kind(0xffffffffu, 0, &output_kind) != SE_GPU_NOT_FOUND) return 6;
|
||||||
|
se_gpu_create_info info{}; info.struct_size = sizeof(info); info.abi_version = SE_GPU_ABI_VERSION;
|
||||||
|
info.width = 64; info.height = 64; info.title_utf8 = "SharpEmu native GPU test";
|
||||||
|
se_gpu_backend* backend{};
|
||||||
|
if (se_gpu_create(&info, &backend) != SE_GPU_OK) return 2;
|
||||||
|
if (argc != 4) { se_gpu_destroy(backend); return 8; }
|
||||||
|
std::vector<char> shader = read_file(argv[1]);
|
||||||
|
se_gpu_compute compute{}; compute.struct_size = sizeof(compute);
|
||||||
|
compute.spirv = {shader.data(), shader.size()}; compute.groups_x = 1; compute.groups_y = 1; compute.groups_z = 1;
|
||||||
|
if (shader.empty() || se_gpu_submit_compute(backend, &compute) != SE_GPU_OK) {
|
||||||
|
se_gpu_destroy(backend); return 9;
|
||||||
|
}
|
||||||
|
std::vector<char> vertex = read_file(argv[2]);
|
||||||
|
std::vector<char> fragment = read_file(argv[3]);
|
||||||
|
se_gpu_draw draw{}; draw.struct_size = sizeof(draw); draw.width = 64; draw.height = 64;
|
||||||
|
draw.vertex_spirv = {vertex.data(), vertex.size()}; draw.pixel_spirv = {fragment.data(), fragment.size()};
|
||||||
|
std::vector<uint32_t> padded_texture(8, 0xff804020u);
|
||||||
|
se_gpu_texture texture{}; texture.struct_size = sizeof(texture); texture.width = 2; texture.height = 2;
|
||||||
|
texture.format = 10; texture.pitch = 4; texture.dst_select = 0x324;
|
||||||
|
texture.rgba_pixels = {padded_texture.data(), padded_texture.size() * sizeof(uint32_t)};
|
||||||
|
draw.textures = &texture; draw.texture_count = 1;
|
||||||
|
draw.vertex_count = 3; draw.instance_count = 1; draw.primitive_type = 4;
|
||||||
|
if (vertex.empty() || fragment.empty() || se_gpu_submit_draw(backend, &draw) != SE_GPU_OK) {
|
||||||
|
se_gpu_destroy(backend); return 10;
|
||||||
|
}
|
||||||
|
std::vector<uint32_t> pixels(64 * 64);
|
||||||
|
for (uint32_t frame = 0; frame < 3; ++frame) {
|
||||||
|
std::fill(pixels.begin(), pixels.end(), 0xff000000u | frame * 0x00303030u);
|
||||||
|
if (se_gpu_present_bgra(backend, pixels.data(), pixels.size() * sizeof(uint32_t), 64, 64, 256) != SE_GPU_OK) {
|
||||||
|
se_gpu_destroy(backend); return 3;
|
||||||
|
}
|
||||||
|
uint32_t close{}; if (se_gpu_poll(backend, &close) != SE_GPU_OK || close) {
|
||||||
|
se_gpu_destroy(backend); return 4;
|
||||||
|
}
|
||||||
|
se_gpu_input input{}; input.struct_size = sizeof(input);
|
||||||
|
if (se_gpu_input_snapshot(backend, &input) != SE_GPU_OK) { se_gpu_destroy(backend); return 7; }
|
||||||
|
}
|
||||||
|
se_gpu_destroy(backend); return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/* Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
#include "sharpemu_gpu_vulkan.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
if (se_gpu_abi_version() != SE_GPU_ABI_VERSION) return 1;
|
||||||
|
|
||||||
|
uint32_t output_kind{};
|
||||||
|
if (se_gpu_render_target_output_kind(10, 4, &output_kind) != SE_GPU_OK || output_kind != 1) return 2;
|
||||||
|
if (se_gpu_render_target_output_kind(10, 5, &output_kind) != SE_GPU_OK || output_kind != 2) return 3;
|
||||||
|
if (se_gpu_render_target_output_kind(0xffffffffu, 0, &output_kind) != SE_GPU_NOT_FOUND) return 4;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#version 450
|
||||||
|
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||||
|
void main() {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location = 0) out vec4 color;
|
||||||
|
layout(set = 0, binding = 1) uniform sampler2D sourceTexture;
|
||||||
|
void main() { color = texture(sourceTexture, gl_FragCoord.xy / vec2(64.0)); }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#version 450
|
||||||
|
vec2 positions[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||||
|
void main() { gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); }
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.Libs.Gpu.Vulkan;
|
using SharpEmu.Libs.Gpu.Vulkan;
|
||||||
|
using SharpEmu.Libs.Gpu.NativeVulkan;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
namespace SharpEmu.Libs.Gpu;
|
||||||
|
|
||||||
@@ -12,7 +13,13 @@ namespace SharpEmu.Libs.Gpu;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class GuestGpu
|
internal static class GuestGpu
|
||||||
{
|
{
|
||||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
|
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () =>
|
||||||
|
string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND"),
|
||||||
|
"native",
|
||||||
|
StringComparison.OrdinalIgnoreCase)
|
||||||
|
? new NativeVulkanGuestGpuBackend()
|
||||||
|
: new VulkanGuestGpuBackend());
|
||||||
|
|
||||||
public static IGuestGpuBackend Current => Instance.Value;
|
public static IGuestGpuBackend Current => Instance.Value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.HLE.Host.Posix;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||||
|
|
||||||
|
internal sealed unsafe class NativeGpuInputSource : IPosixWindowInputSource
|
||||||
|
{
|
||||||
|
internal static NativeGpuInputSource Instance { get; } = new();
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly uint[] _keys = new uint[8];
|
||||||
|
private bool _focused;
|
||||||
|
private bool _gamepadConnected;
|
||||||
|
private HostGamepadState _gamepad;
|
||||||
|
private string? _gamepadName;
|
||||||
|
|
||||||
|
private NativeGpuInputSource() { }
|
||||||
|
|
||||||
|
internal void Attach() => PosixHostInput.SetSource(this);
|
||||||
|
|
||||||
|
internal void Update(NativeVulkanApi.Input* state)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_focused = state->KeyboardFocused != 0;
|
||||||
|
for (var index = 0; index < _keys.Length; ++index) _keys[index] = state->VirtualKeys[index];
|
||||||
|
_gamepadConnected = state->GamepadConnected != 0;
|
||||||
|
_gamepad = new HostGamepadState(
|
||||||
|
_gamepadConnected,
|
||||||
|
(HostGamepadButtons)state->GamepadButtons,
|
||||||
|
state->LeftX,
|
||||||
|
state->LeftY,
|
||||||
|
state->RightX,
|
||||||
|
state->RightY,
|
||||||
|
state->LeftTrigger,
|
||||||
|
state->RightTrigger);
|
||||||
|
_gamepadName = _gamepadConnected
|
||||||
|
? Marshal.PtrToStringUTF8((nint)state->GamepadNameUtf8)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasKeyboardFocus
|
||||||
|
{
|
||||||
|
get { lock (_gate) return _focused; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsKeyDown(int virtualKey)
|
||||||
|
{
|
||||||
|
if ((uint)virtualKey >= 256) return false;
|
||||||
|
lock (_gate) return (_keys[virtualKey / 32] & (1u << (virtualKey % 32))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_gamepadConnected || destination.IsEmpty) return 0;
|
||||||
|
destination[0] = _gamepad;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? DescribeConnectedGamepad()
|
||||||
|
{
|
||||||
|
lock (_gate) return _gamepadConnected ? _gamepadName ?? "SDL gamepad" : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.Libs.Gpu.Vulkan;
|
||||||
|
using SharpEmu.ShaderCompiler.Vulkan;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||||
|
|
||||||
|
internal static unsafe class NativeGpuPacket
|
||||||
|
{
|
||||||
|
internal static NativeGpuResult SubmitDraw(
|
||||||
|
nint backend,
|
||||||
|
VulkanCompiledGuestShader pixelShader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<GuestMemoryBuffer> memoryBuffers,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
uint attributeCount,
|
||||||
|
VulkanCompiledGuestShader? vertexShader,
|
||||||
|
uint vertexCount,
|
||||||
|
uint instanceCount,
|
||||||
|
uint primitiveType,
|
||||||
|
GuestIndexBuffer? indexBuffer,
|
||||||
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers,
|
||||||
|
GuestRenderState? renderState,
|
||||||
|
IReadOnlyList<GuestRenderTarget>? targets,
|
||||||
|
bool publishTargets)
|
||||||
|
{
|
||||||
|
using var storage = new Storage();
|
||||||
|
var nativeTextures = storage.Allocate<NativeVulkanApi.Texture>(textures.Count);
|
||||||
|
for (var index = 0; index < textures.Count; ++index)
|
||||||
|
nativeTextures[index] = Texture(textures[index], storage);
|
||||||
|
var nativeMemory = storage.Allocate<NativeVulkanApi.MemoryBuffer>(memoryBuffers.Count);
|
||||||
|
for (var index = 0; index < memoryBuffers.Count; ++index)
|
||||||
|
nativeMemory[index] = new() { Address = memoryBuffers[index].BaseAddress, Data = storage.Pin(memoryBuffers[index].Data) };
|
||||||
|
var vertices = vertexBuffers ?? [];
|
||||||
|
var nativeVertices = storage.Allocate<NativeVulkanApi.VertexBuffer>(vertices.Count);
|
||||||
|
for (var index = 0; index < vertices.Count; ++index)
|
||||||
|
{
|
||||||
|
var source = vertices[index];
|
||||||
|
nativeVertices[index] = new()
|
||||||
|
{
|
||||||
|
StructSize = (uint)sizeof(NativeVulkanApi.VertexBuffer), Location = source.Location,
|
||||||
|
ComponentCount = source.ComponentCount, DataFormat = source.DataFormat,
|
||||||
|
NumberFormat = source.NumberFormat, Address = source.BaseAddress, Stride = source.Stride,
|
||||||
|
OffsetBytes = source.OffsetBytes, Data = storage.Pin(source.Data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var targetList = targets ?? [];
|
||||||
|
var nativeTargets = storage.Allocate<NativeVulkanApi.RenderTarget>(targetList.Count);
|
||||||
|
for (var index = 0; index < targetList.Count; ++index)
|
||||||
|
{
|
||||||
|
var source = targetList[index];
|
||||||
|
nativeTargets[index] = new()
|
||||||
|
{
|
||||||
|
StructSize = (uint)sizeof(NativeVulkanApi.RenderTarget), Address = source.Address,
|
||||||
|
Width = source.Width, Height = source.Height, Format = source.Format,
|
||||||
|
NumberType = source.NumberType, MipLevels = source.MipLevels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var state = renderState ?? GuestRenderState.Default;
|
||||||
|
var blends = storage.Allocate<NativeVulkanApi.Blend>(state.Blends.Count);
|
||||||
|
for (var index = 0; index < state.Blends.Count; ++index)
|
||||||
|
{
|
||||||
|
var source = state.Blends[index];
|
||||||
|
blends[index] = new()
|
||||||
|
{
|
||||||
|
Enable = source.Enable ? 1u : 0u, ColorSrc = source.ColorSrcFactor,
|
||||||
|
ColorDst = source.ColorDstFactor, ColorFunc = source.ColorFunc,
|
||||||
|
AlphaSrc = source.AlphaSrcFactor, AlphaDst = source.AlphaDstFactor,
|
||||||
|
AlphaFunc = source.AlphaFunc, SeparateAlpha = source.SeparateAlphaBlend ? 1u : 0u,
|
||||||
|
WriteMask = source.WriteMask,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
NativeVulkanApi.IndexBuffer nativeIndex = default;
|
||||||
|
NativeVulkanApi.IndexBuffer* nativeIndexPointer = null;
|
||||||
|
if (indexBuffer is not null)
|
||||||
|
{
|
||||||
|
nativeIndex = new() { Data = storage.Pin(indexBuffer.Data), Is32Bit = indexBuffer.Is32Bit ? 1u : 0u };
|
||||||
|
nativeIndexPointer = &nativeIndex;
|
||||||
|
}
|
||||||
|
NativeVulkanApi.Rect nativeScissor = default; NativeVulkanApi.Rect* scissorPointer = null;
|
||||||
|
if (state.Scissor is { } scissor)
|
||||||
|
{
|
||||||
|
nativeScissor = new() { X = scissor.X, Y = scissor.Y, Width = scissor.Width, Height = scissor.Height };
|
||||||
|
scissorPointer = &nativeScissor;
|
||||||
|
}
|
||||||
|
NativeVulkanApi.Viewport nativeViewport = default; NativeVulkanApi.Viewport* viewportPointer = null;
|
||||||
|
if (state.Viewport is { } viewport)
|
||||||
|
{
|
||||||
|
nativeViewport = new()
|
||||||
|
{
|
||||||
|
X = viewport.X, Y = viewport.Y, Width = viewport.Width, Height = viewport.Height,
|
||||||
|
MinDepth = viewport.MinDepth, MaxDepth = viewport.MaxDepth,
|
||||||
|
};
|
||||||
|
viewportPointer = &nativeViewport;
|
||||||
|
}
|
||||||
|
var draw = new NativeVulkanApi.Draw
|
||||||
|
{
|
||||||
|
StructSize = (uint)sizeof(NativeVulkanApi.Draw),
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
VertexSpirv = storage.Pin(vertexShader?.Spirv ?? SpirvFixedShaders.CreateFullscreenVertex(attributeCount)),
|
||||||
|
PixelSpirv = storage.Pin(pixelShader.Spirv), Textures = nativeTextures,
|
||||||
|
TextureCount = (uint)textures.Count, MemoryBuffers = nativeMemory,
|
||||||
|
MemoryBufferCount = (uint)memoryBuffers.Count, VertexBuffers = nativeVertices,
|
||||||
|
VertexBufferCount = (uint)vertices.Count, Targets = nativeTargets,
|
||||||
|
TargetCount = (uint)targetList.Count, Blends = blends, BlendCount = (uint)state.Blends.Count,
|
||||||
|
IndexBuffer = nativeIndexPointer, Scissor = scissorPointer, ViewportState = viewportPointer,
|
||||||
|
AttributeCount = attributeCount, VertexCount = vertexCount, InstanceCount = instanceCount,
|
||||||
|
PrimitiveType = primitiveType, PublishTargets = publishTargets ? 1u : 0u,
|
||||||
|
};
|
||||||
|
return NativeVulkanApi.SubmitDraw(backend, &draw);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static NativeGpuResult SubmitCompute(
|
||||||
|
nint backend,
|
||||||
|
ulong shaderAddress,
|
||||||
|
VulkanCompiledGuestShader shader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<GuestMemoryBuffer> buffers,
|
||||||
|
uint x, uint y, uint z)
|
||||||
|
{
|
||||||
|
using var storage = new Storage();
|
||||||
|
var nativeTextures = storage.Allocate<NativeVulkanApi.Texture>(textures.Count);
|
||||||
|
for (var index = 0; index < textures.Count; ++index) nativeTextures[index] = Texture(textures[index], storage);
|
||||||
|
var nativeBuffers = storage.Allocate<NativeVulkanApi.MemoryBuffer>(buffers.Count);
|
||||||
|
for (var index = 0; index < buffers.Count; ++index)
|
||||||
|
nativeBuffers[index] = new() { Address = buffers[index].BaseAddress, Data = storage.Pin(buffers[index].Data) };
|
||||||
|
var compute = new NativeVulkanApi.Compute
|
||||||
|
{
|
||||||
|
StructSize = (uint)sizeof(NativeVulkanApi.Compute), ShaderAddress = shaderAddress,
|
||||||
|
Spirv = storage.Pin(shader.Spirv), Textures = nativeTextures, TextureCount = (uint)textures.Count,
|
||||||
|
MemoryBuffers = nativeBuffers, MemoryBufferCount = (uint)buffers.Count,
|
||||||
|
GroupsX = x, GroupsY = y, GroupsZ = z,
|
||||||
|
};
|
||||||
|
return NativeVulkanApi.SubmitCompute(backend, &compute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeVulkanApi.Texture Texture(GuestDrawTexture source, Storage storage)
|
||||||
|
{
|
||||||
|
var result = new NativeVulkanApi.Texture
|
||||||
|
{
|
||||||
|
StructSize = (uint)sizeof(NativeVulkanApi.Texture), Address = source.Address,
|
||||||
|
Width = source.Width, Height = source.Height, Format = source.Format,
|
||||||
|
NumberType = source.NumberType, RgbaPixels = storage.Pin(source.RgbaPixels),
|
||||||
|
IsFallback = source.IsFallback ? 1u : 0u, IsStorage = source.IsStorage ? 1u : 0u,
|
||||||
|
MipLevels = source.MipLevels, MipLevel = source.MipLevel, Pitch = source.Pitch,
|
||||||
|
TileMode = source.TileMode, DstSelect = source.DstSelect,
|
||||||
|
};
|
||||||
|
result.SamplerState.Words[0] = source.Sampler.Word0;
|
||||||
|
result.SamplerState.Words[1] = source.Sampler.Word1;
|
||||||
|
result.SamplerState.Words[2] = source.Sampler.Word2;
|
||||||
|
result.SamplerState.Words[3] = source.Sampler.Word3;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Storage : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<GCHandle> _pins = [];
|
||||||
|
private readonly List<nint> _allocations = [];
|
||||||
|
internal NativeVulkanApi.Bytes Pin(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length == 0) return default;
|
||||||
|
var pin = GCHandle.Alloc(data, GCHandleType.Pinned); _pins.Add(pin);
|
||||||
|
return new() { Data = (void*)pin.AddrOfPinnedObject(), Size = (nuint)data.Length };
|
||||||
|
}
|
||||||
|
internal T* Allocate<T>(int count) where T : unmanaged
|
||||||
|
{
|
||||||
|
if (count == 0) return null;
|
||||||
|
var pointer = NativeMemory.AllocZeroed((nuint)count, (nuint)sizeof(T));
|
||||||
|
if (pointer is null) throw new OutOfMemoryException();
|
||||||
|
_allocations.Add((nint)pointer); return (T*)pointer;
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var pin in _pins) pin.Free();
|
||||||
|
foreach (var allocation in _allocations) NativeMemory.Free((void*)allocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||||
|
|
||||||
|
internal enum NativeGpuResult
|
||||||
|
{
|
||||||
|
Success = 0,
|
||||||
|
NotFound = 1,
|
||||||
|
NotReady = 2,
|
||||||
|
InvalidArgument = -1,
|
||||||
|
IncompatibleAbi = -2,
|
||||||
|
PlatformError = -3,
|
||||||
|
VulkanError = -4,
|
||||||
|
OutOfMemory = -5,
|
||||||
|
InternalError = -6,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static unsafe partial class NativeVulkanApi
|
||||||
|
{
|
||||||
|
internal const uint AbiVersion = 1;
|
||||||
|
private const string Library = "sharpemu_gpu_vulkan";
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct CreateInfo
|
||||||
|
{
|
||||||
|
internal uint StructSize;
|
||||||
|
internal uint AbiVersion;
|
||||||
|
internal uint Width;
|
||||||
|
internal uint Height;
|
||||||
|
internal uint EnableValidation;
|
||||||
|
internal byte* TitleUtf8;
|
||||||
|
internal delegate* unmanaged[Cdecl]<int, byte*, void*, void> Log;
|
||||||
|
internal void* LogUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)] internal struct Bytes { internal void* Data; internal nuint Size; }
|
||||||
|
[StructLayout(LayoutKind.Sequential)] internal struct Sampler { internal fixed uint Words[4]; }
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct Texture
|
||||||
|
{
|
||||||
|
internal uint StructSize; internal ulong Address; internal uint Width, Height, Format, NumberType;
|
||||||
|
internal Bytes RgbaPixels; internal uint IsFallback, IsStorage, MipLevels, MipLevel;
|
||||||
|
internal uint Pitch, TileMode, DstSelect; internal Sampler SamplerState;
|
||||||
|
}
|
||||||
|
[StructLayout(LayoutKind.Sequential)] internal struct MemoryBuffer { internal ulong Address; internal Bytes Data; }
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct VertexBuffer
|
||||||
|
{
|
||||||
|
internal uint StructSize, Location, ComponentCount, DataFormat, NumberFormat;
|
||||||
|
internal ulong Address; internal uint Stride, OffsetBytes; internal Bytes Data;
|
||||||
|
}
|
||||||
|
[StructLayout(LayoutKind.Sequential)] internal struct IndexBuffer { internal Bytes Data; internal uint Is32Bit; }
|
||||||
|
[StructLayout(LayoutKind.Sequential)] internal struct Rect { internal int X, Y; internal uint Width, Height; }
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct Viewport { internal float X, Y, Width, Height, MinDepth, MaxDepth; }
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct Blend
|
||||||
|
{
|
||||||
|
internal uint Enable, ColorSrc, ColorDst, ColorFunc, AlphaSrc, AlphaDst, AlphaFunc;
|
||||||
|
internal uint SeparateAlpha, WriteMask;
|
||||||
|
}
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct RenderTarget
|
||||||
|
{
|
||||||
|
internal uint StructSize; internal ulong Address;
|
||||||
|
internal uint Width, Height, Format, NumberType, MipLevels;
|
||||||
|
}
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct Draw
|
||||||
|
{
|
||||||
|
internal uint StructSize, Width, Height; internal Bytes VertexSpirv, PixelSpirv;
|
||||||
|
internal Texture* Textures; internal uint TextureCount;
|
||||||
|
internal MemoryBuffer* MemoryBuffers; internal uint MemoryBufferCount;
|
||||||
|
internal VertexBuffer* VertexBuffers; internal uint VertexBufferCount;
|
||||||
|
internal RenderTarget* Targets; internal uint TargetCount;
|
||||||
|
internal Blend* Blends; internal uint BlendCount;
|
||||||
|
internal IndexBuffer* IndexBuffer; internal Rect* Scissor; internal Viewport* ViewportState;
|
||||||
|
internal uint AttributeCount, VertexCount, InstanceCount, PrimitiveType, PublishTargets;
|
||||||
|
}
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct Compute
|
||||||
|
{
|
||||||
|
internal uint StructSize; internal ulong ShaderAddress; internal Bytes Spirv;
|
||||||
|
internal Texture* Textures; internal uint TextureCount;
|
||||||
|
internal MemoryBuffer* MemoryBuffers; internal uint MemoryBufferCount;
|
||||||
|
internal uint GroupsX, GroupsY, GroupsZ;
|
||||||
|
}
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct Input
|
||||||
|
{
|
||||||
|
internal uint StructSize, KeyboardFocused;
|
||||||
|
internal fixed uint VirtualKeys[8];
|
||||||
|
internal uint GamepadConnected, GamepadButtons;
|
||||||
|
internal byte LeftX, LeftY, RightX, RightY, LeftTrigger, RightTrigger;
|
||||||
|
internal fixed byte Reserved[2]; internal fixed byte GamepadNameUtf8[128];
|
||||||
|
}
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_abi_version")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial uint GetAbiVersion();
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_last_error")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
private static partial byte* LastError(nint backend);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_create")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult Create(CreateInfo* info, out nint backend);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_destroy")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial void Destroy(nint backend);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_poll")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult Poll(nint backend, out uint shouldClose);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_input_snapshot")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult InputSnapshot(nint backend, Input* input);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_present_bgra")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult PresentBgra(
|
||||||
|
nint backend,
|
||||||
|
void* pixels,
|
||||||
|
nuint size,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
uint pitch);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_submit_draw")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult SubmitDraw(nint backend, Draw* draw);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_submit_compute")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult SubmitCompute(nint backend, Compute* compute);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_register_display_buffer")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult RegisterDisplayBuffer(nint backend, ulong address, uint format);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_present_guest_image")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult PresentGuestImage(
|
||||||
|
nint backend, ulong address, uint width, uint height, uint pitch);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_has_guest_image")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult HasGuestImage(nint backend, ulong address, uint format, uint numberType);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_blit_guest_image")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult BlitGuestImage(
|
||||||
|
nint backend, ulong sourceAddress, uint sourceWidth, uint sourceHeight, uint sourceFormat,
|
||||||
|
ulong destinationAddress, uint destinationWidth, uint destinationHeight, uint destinationFormat);
|
||||||
|
|
||||||
|
[LibraryImport(Library, EntryPoint = "se_gpu_render_target_output_kind")]
|
||||||
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
internal static partial NativeGpuResult RenderTargetOutputKind(uint format, uint numberType, out uint outputKind);
|
||||||
|
|
||||||
|
internal static string GetError(nint backend)
|
||||||
|
{
|
||||||
|
var pointer = LastError(backend);
|
||||||
|
return pointer is null ? "Unknown native GPU error" : Marshal.PtrToStringUTF8((nint)pointer)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.Libs.Gpu.Vulkan;
|
||||||
|
using SharpEmu.ShaderCompiler;
|
||||||
|
using SharpEmu.ShaderCompiler.Vulkan;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||||
|
|
||||||
|
/// <summary>Native C++ Vulkan implementation of the guest-domain GPU seam.</summary>
|
||||||
|
internal sealed unsafe class NativeVulkanGuestGpuBackend : IGuestGpuBackend
|
||||||
|
{
|
||||||
|
private readonly object _startGate = new();
|
||||||
|
private readonly BlockingCollection<Action<nint>> _commands = new(new ConcurrentQueue<Action<nint>>(), 256);
|
||||||
|
private readonly ManualResetEventSlim _ready = new(false);
|
||||||
|
private Thread? _thread;
|
||||||
|
private Exception? _startError;
|
||||||
|
|
||||||
|
public void EnsureStarted(uint width, uint height)
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0) return;
|
||||||
|
lock (_startGate)
|
||||||
|
{
|
||||||
|
if (_thread is null)
|
||||||
|
{
|
||||||
|
_thread = new Thread(() => Run(width, height))
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu native Vulkan",
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ready.Wait();
|
||||||
|
if (_startError is not null) throw new InvalidOperationException("Native Vulkan startup failed", _startError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCompileVertexShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||||
|
out IGuestCompiledShader? shader, out string error, int globalBufferBase = 0,
|
||||||
|
int totalGlobalBufferCount = -1, int imageBindingBase = 0, int scalarRegisterBufferIndex = -1)
|
||||||
|
{
|
||||||
|
shader = null;
|
||||||
|
if (!Gen5SpirvTranslator.TryCompileVertexShader(state, evaluation, out var compiled, out error,
|
||||||
|
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex)) return false;
|
||||||
|
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCompilePixelShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||||
|
IReadOnlyList<Gen5PixelOutputBinding> outputs, out IGuestCompiledShader? shader, out string error,
|
||||||
|
int globalBufferBase = 0, int totalGlobalBufferCount = -1, int imageBindingBase = 0,
|
||||||
|
int scalarRegisterBufferIndex = -1)
|
||||||
|
{
|
||||||
|
shader = null;
|
||||||
|
if (!Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, outputs, out var compiled, out error,
|
||||||
|
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex)) return false;
|
||||||
|
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCompileComputeShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||||
|
uint localSizeX, uint localSizeY, uint localSizeZ, out IGuestCompiledShader? shader, out string error)
|
||||||
|
{
|
||||||
|
shader = null;
|
||||||
|
if (!Gen5SpirvTranslator.TryCompileComputeShader(state, evaluation, localSizeX, localSizeY, localSizeZ,
|
||||||
|
out var compiled, out error)) return false;
|
||||||
|
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HideSplashScreen() { }
|
||||||
|
|
||||||
|
public void Submit(byte[] bgraFrame, uint width, uint height)
|
||||||
|
{
|
||||||
|
if (bgraFrame.Length != checked((int)(width * height * 4))) return;
|
||||||
|
EnsureStarted(width, height);
|
||||||
|
Enqueue(handle =>
|
||||||
|
{
|
||||||
|
fixed (byte* pixels = bgraFrame)
|
||||||
|
Check(handle, NativeVulkanApi.PresentBgra(handle, pixels, (nuint)bgraFrame.Length, width, height, width * 4));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height)
|
||||||
|
{
|
||||||
|
if (drawKind != GuestDrawKind.FullscreenBarycentric || width == 0 || height == 0) return;
|
||||||
|
EnsureStarted(width, height);
|
||||||
|
var pixel = new VulkanCompiledGuestShader(SpirvFixedShaders.CreateBarycentricFragment());
|
||||||
|
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, pixel, [], [],
|
||||||
|
width, height, 1, null, 3, 1, 4, null, null, null, null, false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SubmitTranslatedDraw(IGuestCompiledShader pixelShader, IReadOnlyList<GuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers, uint width, uint height, uint attributeCount,
|
||||||
|
IGuestCompiledShader? vertexShader = null, uint vertexCount = 3, uint instanceCount = 1,
|
||||||
|
uint primitiveType = 4, GuestIndexBuffer? indexBuffer = null,
|
||||||
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null, GuestRenderState? renderState = null)
|
||||||
|
{
|
||||||
|
EnsureStarted(width, height);
|
||||||
|
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||||
|
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||||
|
var vertexCopy = vertexBuffers?.ToArray();
|
||||||
|
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||||
|
width, height, attributeCount, vs, vertexCount, instanceCount, primitiveType, indexBuffer,
|
||||||
|
vertexCopy, renderState, null, false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SubmitOffscreenTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
|
uint attributeCount, IReadOnlyList<GuestRenderTarget> targets, IGuestCompiledShader? vertexShader = null,
|
||||||
|
uint vertexCount = 3, uint instanceCount = 1, uint primitiveType = 4,
|
||||||
|
GuestIndexBuffer? indexBuffer = null, IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
|
GuestRenderState? renderState = null)
|
||||||
|
{
|
||||||
|
if (targets.Count == 0) return;
|
||||||
|
EnsureStarted(targets[0].Width, targets[0].Height);
|
||||||
|
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||||
|
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||||
|
var targetCopy = targets.ToArray(); var vertexCopy = vertexBuffers?.ToArray();
|
||||||
|
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||||
|
targetCopy[0].Width, targetCopy[0].Height, attributeCount, vs, vertexCount, instanceCount,
|
||||||
|
primitiveType, indexBuffer, vertexCopy, renderState, targetCopy, true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SubmitStorageTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
|
uint attributeCount, uint width, uint height)
|
||||||
|
{
|
||||||
|
EnsureStarted(width, height); var ps = Spirv(pixelShader);
|
||||||
|
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||||
|
GuestRenderTarget[] targets = [new(0, width, height, 12, 7)];
|
||||||
|
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||||
|
width, height, attributeCount, null, 3, 1, 4, null, null, null, targets, false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SubmitComputeDispatch(ulong shaderAddress, IGuestCompiledShader computeShader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
|
uint groupCountX, uint groupCountY, uint groupCountZ)
|
||||||
|
{
|
||||||
|
EnsureStarted(1280, 720); var shader = Spirv(computeShader);
|
||||||
|
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||||
|
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitCompute(handle, shaderAddress, shader,
|
||||||
|
textureCopy, memoryCopy, groupCountX, groupCountY, groupCountZ)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TrySubmitGuestImage(ulong address, uint width, uint height, uint pitchInPixel)
|
||||||
|
{
|
||||||
|
EnsureStarted(width, height);
|
||||||
|
return Invoke(handle => NativeVulkanApi.PresentGuestImage(handle, address, width, height, pitchInPixel)) ==
|
||||||
|
NativeGpuResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
|
||||||
|
{
|
||||||
|
EnsureStarted(1280, 720);
|
||||||
|
Enqueue(handle => Check(handle, NativeVulkanApi.RegisterDisplayBuffer(handle, address, guestFormat)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType)
|
||||||
|
{
|
||||||
|
EnsureStarted(1280, 720);
|
||||||
|
return Invoke(handle => NativeVulkanApi.HasGuestImage(handle, address, format, numberType)) ==
|
||||||
|
NativeGpuResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TrySubmitGuestImageBlit(ulong sourceAddress, uint sourceWidth, uint sourceHeight,
|
||||||
|
uint sourceFormat, ulong destinationAddress, uint destinationWidth, uint destinationHeight,
|
||||||
|
uint destinationFormat)
|
||||||
|
{
|
||||||
|
EnsureStarted(destinationWidth, destinationHeight);
|
||||||
|
return Invoke(handle => NativeVulkanApi.BlitGuestImage(handle, sourceAddress, sourceWidth, sourceHeight,
|
||||||
|
sourceFormat, destinationAddress, destinationWidth, destinationHeight, destinationFormat)) ==
|
||||||
|
NativeGpuResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType,
|
||||||
|
out Gen5PixelOutputKind outputKind)
|
||||||
|
{
|
||||||
|
var result = NativeVulkanApi.RenderTargetOutputKind(dataFormat, numberType, out var nativeKind);
|
||||||
|
outputKind = (Gen5PixelOutputKind)nativeKind; return result == NativeGpuResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run(uint width, uint height)
|
||||||
|
{
|
||||||
|
nint backend = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (NativeVulkanApi.GetAbiVersion() != NativeVulkanApi.AbiVersion)
|
||||||
|
throw new InvalidOperationException("Native Vulkan ABI version mismatch");
|
||||||
|
var title = Marshal.StringToCoTaskMemUTF8("SharpEmu");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = new NativeVulkanApi.CreateInfo
|
||||||
|
{
|
||||||
|
StructSize = (uint)sizeof(NativeVulkanApi.CreateInfo), AbiVersion = NativeVulkanApi.AbiVersion,
|
||||||
|
Width = width, Height = height, TitleUtf8 = (byte*)title,
|
||||||
|
EnableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1" ? 1u : 0u,
|
||||||
|
};
|
||||||
|
var result = NativeVulkanApi.Create(&info, out backend);
|
||||||
|
if (result != NativeGpuResult.Success) throw new InvalidOperationException(NativeVulkanApi.GetError(0));
|
||||||
|
}
|
||||||
|
finally { Marshal.FreeCoTaskMem(title); }
|
||||||
|
_ready.Set();
|
||||||
|
NativeGpuInputSource.Instance.Attach();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (_commands.TryTake(out var command, 8))
|
||||||
|
{
|
||||||
|
command(backend);
|
||||||
|
for (var drained = 1; drained < 128 && _commands.TryTake(out command); ++drained)
|
||||||
|
command(backend);
|
||||||
|
}
|
||||||
|
var result = NativeVulkanApi.Poll(backend, out var shouldClose);
|
||||||
|
if (result != NativeGpuResult.Success || shouldClose != 0) break;
|
||||||
|
var input = new NativeVulkanApi.Input { StructSize = (uint)sizeof(NativeVulkanApi.Input) };
|
||||||
|
if (NativeVulkanApi.InputSnapshot(backend, &input) == NativeGpuResult.Success)
|
||||||
|
NativeGpuInputSource.Instance.Update(&input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_startError ??= exception;
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] Native Vulkan backend failed: {exception}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_ready.Set();
|
||||||
|
if (backend != 0) NativeVulkanApi.Destroy(backend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Enqueue(Action<nint> command)
|
||||||
|
{
|
||||||
|
if (!_commands.TryAdd(command)) Console.Error.WriteLine("[LOADER][WARN] Native GPU queue is full; dropping work");
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeGpuResult Invoke(Func<nint, NativeGpuResult> operation)
|
||||||
|
{
|
||||||
|
var completion = new TaskCompletionSource<NativeGpuResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
Enqueue(handle =>
|
||||||
|
{
|
||||||
|
try { completion.SetResult(operation(handle)); }
|
||||||
|
catch (Exception exception) { completion.SetException(exception); }
|
||||||
|
});
|
||||||
|
return completion.Task.GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Check(nint backend, NativeGpuResult result)
|
||||||
|
{
|
||||||
|
if (result is NativeGpuResult.Success or NativeGpuResult.NotReady) return;
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] Native GPU operation failed: {result}: {NativeVulkanApi.GetError(backend)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VulkanCompiledGuestShader Spirv(IGuestCompiledShader shader) =>
|
||||||
|
shader as VulkanCompiledGuestShader ?? throw new InvalidOperationException(
|
||||||
|
$"Shader type {shader.GetType().Name} was not compiled by the native Vulkan backend");
|
||||||
|
}
|
||||||
@@ -37,5 +37,43 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<NativeGpuProjectDir>$(MSBuildThisFileDirectory)..\SharpEmu.Gpu.Vulkan.Native</NativeGpuProjectDir>
|
||||||
|
<NativeGpuBuildDir>$(RepoRoot)artifacts\native\gpu-vulkan</NativeGpuBuildDir>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
|
||||||
|
<NativeGpuLibrary>$(NativeGpuBuildDir)\bin\sharpemu_gpu_vulkan.dll</NativeGpuLibrary>
|
||||||
|
<NativeGpuFileName>sharpemu_gpu_vulkan.dll</NativeGpuFileName>
|
||||||
|
<NativeGpuSdlLibrary>$(NativeGpuBuildDir)\bin\SDL3.dll</NativeGpuSdlLibrary>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
|
||||||
|
<NativeGpuLibrary>$(NativeGpuBuildDir)\bin\libsharpemu_gpu_vulkan.so</NativeGpuLibrary>
|
||||||
|
<NativeGpuFileName>libsharpemu_gpu_vulkan.so</NativeGpuFileName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('OSX'))">
|
||||||
|
<NativeGpuLibrary>$(NativeGpuBuildDir)\bin\libsharpemu_gpu_vulkan.dylib</NativeGpuLibrary>
|
||||||
|
<NativeGpuFileName>libsharpemu_gpu_vulkan.dylib</NativeGpuFileName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(NativeGpuLibrary)' != ''">
|
||||||
|
<None Include="$(NativeGpuLibrary)"
|
||||||
|
Link="$(NativeGpuFileName)"
|
||||||
|
CopyToOutputDirectory="PreserveNewest"
|
||||||
|
CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
<None Include="$(NativeGpuSdlLibrary)"
|
||||||
|
Condition="'$(NativeGpuSdlLibrary)' != ''"
|
||||||
|
Link="SDL3.dll"
|
||||||
|
CopyToOutputDirectory="PreserveNewest"
|
||||||
|
CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="BuildNativeGuestGpu" BeforeTargets="Build" Condition="'$(NativeGpuLibrary)' != ''">
|
||||||
|
<Exec Command="cmake -S "$(NativeGpuProjectDir)" -B "$(NativeGpuBuildDir)" -DCMAKE_BUILD_TYPE=$(Configuration) -DBUILD_TESTING=OFF" />
|
||||||
|
<Exec Command="cmake --build "$(NativeGpuBuildDir)" --config $(Configuration)" />
|
||||||
|
<Copy SourceFiles="$(NativeGpuLibrary)" DestinationFolder="$(TargetDir)" SkipUnchangedFiles="true" />
|
||||||
|
<Copy SourceFiles="$(NativeGpuSdlLibrary)"
|
||||||
|
DestinationFolder="$(TargetDir)"
|
||||||
|
SkipUnchangedFiles="true"
|
||||||
|
Condition="'$(NativeGpuSdlLibrary)' != ''" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,6 +5,36 @@ namespace SharpEmu.ShaderCompiler.Vulkan;
|
|||||||
|
|
||||||
public static class SpirvFixedShaders
|
public static class SpirvFixedShaders
|
||||||
{
|
{
|
||||||
|
/// <summary>Fragment half of the fixed fullscreen barycentric diagnostic draw.</summary>
|
||||||
|
public static byte[] CreateBarycentricFragment()
|
||||||
|
{
|
||||||
|
var module = new SpirvModuleBuilder();
|
||||||
|
module.AddCapability(SpirvCapability.Shader);
|
||||||
|
var voidType = module.TypeVoid();
|
||||||
|
var floatType = module.TypeFloat(32);
|
||||||
|
var vec4Type = module.TypeVector(floatType, 4);
|
||||||
|
var inputPointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
|
||||||
|
var outputPointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||||
|
var barycentric = module.AddGlobalVariable(inputPointer, SpirvStorageClass.Input);
|
||||||
|
module.AddName(barycentric, "barycentric");
|
||||||
|
module.AddDecoration(barycentric, SpirvDecoration.Location, 0);
|
||||||
|
module.AddDecoration(barycentric, SpirvDecoration.NoPerspective);
|
||||||
|
var output = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
|
||||||
|
module.AddName(output, "outColor");
|
||||||
|
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||||
|
var functionType = module.TypeFunction(voidType);
|
||||||
|
var main = module.BeginFunction(voidType, functionType);
|
||||||
|
module.AddName(main, "main");
|
||||||
|
module.AddLabel();
|
||||||
|
var value = module.AddInstruction(SpirvOp.Load, vec4Type, barycentric);
|
||||||
|
module.AddStatement(SpirvOp.Store, output, value);
|
||||||
|
module.AddStatement(SpirvOp.Return);
|
||||||
|
module.EndFunction();
|
||||||
|
module.AddEntryPoint(SpirvExecutionModel.Fragment, main, "main", [barycentric, output]);
|
||||||
|
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||||
|
return module.Build();
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||||
{
|
{
|
||||||
var module = new SpirvModuleBuilder();
|
var module = new SpirvModuleBuilder();
|
||||||
|
|||||||
Reference in New Issue
Block a user