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