feat(host): vendor PyroWave + minimal Granite subset as crates/pyrowave-sys
Phase 0 of design/pyrowave-codec-plan.md — the opt-in wired-LAN ultra-low- latency codec. Vendored at upstream 509e4f88 (API 0.4.0, Granite 44362775, volk + vulkan-headers pins in PUNKTFUNK-VENDOR.txt), pruned to the 6.6 MB the standalone no-renderer build needs; scripts/vendor-pyrowave.sh reproduces the tree (a pin bump is protocol-affecting, plan §4.2). build.rs drives the wrapper CMakeLists (static archives incl. a static C-API lib upstream only ships shared) + bindgen over pyrowave.h; Linux and Windows only, empty stub elsewhere (Apple gets a native Metal port, §4.7). Offline-safe by construction: no network, no system lib, vendored Vulkan headers — same model as the opus dep (flatpak builder has no network). Phase-0 validation on .21 (RTX 5070 Ti, driver 610.43.03): - upstream pyrowave-c-test + interop test (incl. dmabuf/DRM-modifier Vulkan<->Vulkan) pass, from the pristine AND the pruned tree - GPU kernel times at ~1.6 bpp noise: encode/decode 0.090/0.042 ms @800p, 0.146/0.067 @1080p, 0.226/0.103 @1440p, 0.477/0.201 @4K — order of magnitude under NVENC's 1-2 ms retrieve, CBR lands within ~100 B of target - cargo test -p pyrowave-sys green (static link + API-version pin check) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+53
@@ -0,0 +1,53 @@
|
||||
# Must be static due to Granite::application_create() feedback.
|
||||
|
||||
if (ANDROID)
|
||||
add_granite_internal_static_lib(granite-platform application_android.cpp)
|
||||
find_package(game-activity REQUIRED CONFIG)
|
||||
find_package(games-controller REQUIRED CONFIG)
|
||||
find_package(games-frame-pacing REQUIRED CONFIG)
|
||||
target_link_libraries(granite-platform PRIVATE granite-rapidjson
|
||||
game-activity::game-activity_static
|
||||
games-controller::paddleboat games-frame-pacing::swappy)
|
||||
|
||||
target_compile_definitions(granite-platform PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
|
||||
if (GRANITE_ANDROID_SWAPPY)
|
||||
target_compile_definitions(granite-platform PRIVATE HAVE_SWAPPY)
|
||||
endif()
|
||||
if (GRANITE_ANDROID_APK_FILESYSTEM)
|
||||
target_compile_definitions(granite-platform PRIVATE ANDROID_APK_FILESYSTEM)
|
||||
endif()
|
||||
elseif (${GRANITE_PLATFORM} MATCHES "libretro")
|
||||
add_granite_internal_static_lib(granite-platform
|
||||
application_libretro.cpp
|
||||
application_libretro_utils.cpp
|
||||
application_libretro_utils.hpp)
|
||||
target_compile_definitions(granite-platform PUBLIC HAVE_LIBRETRO)
|
||||
target_include_directories(granite-platform PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/libretro)
|
||||
elseif (${GRANITE_PLATFORM} MATCHES "SDL")
|
||||
add_granite_internal_static_lib(granite-platform application_sdl3.cpp)
|
||||
|
||||
if (GRANITE_SYSTEM_SDL)
|
||||
find_package(SDL3 REQUIRED)
|
||||
target_link_libraries(granite-platform PRIVATE SDL3::SDL3-shared)
|
||||
else()
|
||||
target_link_libraries(granite-platform PRIVATE SDL3-static)
|
||||
if (NOT WIN32)
|
||||
target_link_libraries(granite-platform PRIVATE dl)
|
||||
endif()
|
||||
endif()
|
||||
target_link_libraries(granite-platform PRIVATE granite-input-sdl)
|
||||
elseif (${GRANITE_PLATFORM} MATCHES "headless")
|
||||
add_granite_internal_static_lib(granite-platform application_headless_wrapper.cpp)
|
||||
elseif (${GRANITE_PLATFORM} MATCHES "null")
|
||||
add_granite_internal_static_lib(granite-platform application_null.cpp)
|
||||
else()
|
||||
message(FATAL "GRANITE_PLATFORM is not set.")
|
||||
endif()
|
||||
|
||||
if (NOT (${GRANITE_PLATFORM} MATCHES "null"))
|
||||
target_link_libraries(granite-platform PRIVATE granite-application-interface-query granite-application granite-input granite-application-global-init)
|
||||
endif()
|
||||
|
||||
if (GRANITE_AUDIO)
|
||||
target_link_libraries(granite-platform PRIVATE granite-audio)
|
||||
endif()
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
namespace = 'net.themaister.granite'
|
||||
compileSdkVersion = 34
|
||||
buildFeatures.prefab = true
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion = 26
|
||||
targetSdkVersion = 34
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'com.google.android.material:material:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core:1.9.0'
|
||||
implementation 'androidx.games:games-activity:4.0.0'
|
||||
}
|
||||
crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/external_layers/README.txt
Vendored
+1
@@ -0,0 +1 @@
|
||||
Place {arm64-v8a,armeabi-v7a}/libVkLayer_*.so here to have it bundled in the APK.
|
||||
crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/AndroidManifest.xml
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="$$VERSION_CODE$$"
|
||||
android:versionName="$$VERSION_NAME$$">
|
||||
|
||||
<!-- Require Vulkan 1.1 -->
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true"/>
|
||||
<uses-feature android:name="android.hardware.vulkan.level" android:version="0" android:required="true"/>
|
||||
|
||||
<application android:label="@string/app_name" android:icon="@drawable/$$ICON$$">
|
||||
<activity android:name="$$ACTIVITY_NAME$$"
|
||||
android:theme="@style/Application.Fullscreen"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="landscape"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name">
|
||||
<meta-data android:name="android.app.lib_name" android:value="$$NATIVE_TARGET$$" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
</manifest>
|
||||
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace = '$$NAMESPACE$$'
|
||||
compileSdkVersion = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion = 26
|
||||
targetSdkVersion = 34
|
||||
|
||||
ndk {
|
||||
abiFilters = [$$ABIS$$]
|
||||
}
|
||||
}
|
||||
|
||||
ndkVersion = '29.0.14206865'
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments = ["-DANDROID_TOOLCHAIN=clang",
|
||||
"-DANDROID_STL=c++_shared",
|
||||
"-DANDROID_ARM_MODE=arm",
|
||||
"-DANDROID_CPP_FEATURES=exceptions",
|
||||
"-DGRANITE_SHADER_COMPILER_OPTIMIZE=$$SHADER_OPTIMIZE$$",
|
||||
"-DGRANITE_VULKAN_FOSSILIZE=$$FOSSILIZE$$",
|
||||
"-DGRANITE_ANDROID_SWAPPY=$$SWAPPY$$",
|
||||
"-DCMAKE_BUILD_TYPE=Debug",
|
||||
"-DANDROID_PLATFORM=android-26",
|
||||
"-DGRANITE_SHIPPING=ON",
|
||||
"-DGRANITE_AUDIO=$$AUDIO$$",
|
||||
"-DGRANITE_BULLET=$$PHYSICS$$",
|
||||
"-DANDROID_ARM_NEON=ON",
|
||||
"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF",
|
||||
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
|
||||
|
||||
targets = ["$$TARGET$$"]
|
||||
}
|
||||
}
|
||||
jniDebuggable = true
|
||||
}
|
||||
release {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments = ["-DANDROID_TOOLCHAIN=clang",
|
||||
"-DANDROID_STL=c++_shared",
|
||||
"-DANDROID_ARM_MODE=arm",
|
||||
"-DANDROID_CPP_FEATURES=exceptions",
|
||||
"-DGRANITE_SHADER_COMPILER_OPTIMIZE=$$SHADER_OPTIMIZE$$",
|
||||
"-DGRANITE_VULKAN_FOSSILIZE=$$FOSSILIZE$$",
|
||||
"-DGRANITE_ANDROID_SWAPPY=$$SWAPPY$$",
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DANDROID_PLATFORM=android-26",
|
||||
"-DGRANITE_SHIPPING=ON",
|
||||
"-DGRANITE_AUDIO=$$AUDIO$$",
|
||||
"-DGRANITE_BULLET=$$PHYSICS$$",
|
||||
"-DANDROID_ARM_NEON=ON",
|
||||
"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF",
|
||||
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
|
||||
|
||||
targets = ["$$TARGET$$"]
|
||||
}
|
||||
}
|
||||
debuggable = true
|
||||
signingConfig = signingConfigs.debug
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
resources.srcDirs = ['res']
|
||||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['$$ASSETS$$', '$$GRANITE_ASSETS$$']
|
||||
jniLibs.srcDirs = ['$$EXTERNAL_JNI$$']
|
||||
$$GRANITE_AUX_ASSETS$$
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = "$$CMAKELISTS$$"
|
||||
version = "3.22.0+"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
prefab = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(':granite')
|
||||
implementation 'androidx.games:games-activity:4.0.0'
|
||||
implementation 'androidx.games:games-controller:2.0.2'
|
||||
implementation 'androidx.games:games-frame-pacing:2.1.3'
|
||||
$$EXTRA_DEPENDENCIES$$
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
android.useAndroidX=true
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
include 'granite'
|
||||
project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
|
||||
include '$$APP$$'
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
include 'granite'
|
||||
project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
|
||||
include 'custom'
|
||||
project(':custom').projectDir = file('$$ANDROID_ACTIVITY_PATH$$')
|
||||
include '$$APP$$'
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:9.0.0-rc01'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.themaister.granite;
|
||||
|
||||
import com.google.androidgamesdk.GameActivity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Display;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
public class GraniteActivity extends GameActivity
|
||||
{
|
||||
private final static String TAG = "Granite";
|
||||
|
||||
private void hideSystemUI()
|
||||
{
|
||||
// This will put the game behind any cutouts and waterfalls on devices which have
|
||||
// them, so the corresponding insets will be non-zero.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||
{
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||
|
||||
// From API 30 onwards, this is the recommended way to hide the system UI, rather than
|
||||
// using View.setSystemUiVisibility.
|
||||
View decorView = getWindow().getDecorView();
|
||||
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(),
|
||||
decorView);
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||
controller.hide(WindowInsetsCompat.Type.displayCutout());
|
||||
controller.setSystemBarsBehavior(
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
else
|
||||
{
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState)
|
||||
{
|
||||
// When true, the app will fit inside any system UI windows.
|
||||
// When false, we render behind any system UI windows.
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
hideSystemUI();
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
super.onCreate(savedState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus)
|
||||
{
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus && Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
|
||||
hideSystemUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed()
|
||||
{
|
||||
// Do nothing. We catch this inside native code instead.
|
||||
}
|
||||
|
||||
public int getDisplayRotation()
|
||||
{
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
if (display == null)
|
||||
return 0;
|
||||
|
||||
return display.getRotation();
|
||||
}
|
||||
|
||||
public int getAudioNativeSampleRate()
|
||||
{
|
||||
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
if (am == null)
|
||||
return 0;
|
||||
|
||||
String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
|
||||
if (sampleRate == null)
|
||||
return 0;
|
||||
|
||||
int rate = Integer.parseInt(sampleRate);
|
||||
return rate;
|
||||
}
|
||||
|
||||
public int getAudioNativeBlockFrames()
|
||||
{
|
||||
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
if (am == null)
|
||||
return 0;
|
||||
|
||||
String frames = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
|
||||
if (frames == null)
|
||||
return 0;
|
||||
|
||||
int count = Integer.parseInt(frames);
|
||||
return count;
|
||||
}
|
||||
|
||||
public String getCommandLineArgument(String key)
|
||||
{
|
||||
Intent intent = getIntent();
|
||||
if (intent == null)
|
||||
return "";
|
||||
String extra = intent.getStringExtra(key);
|
||||
if (extra == null)
|
||||
return "";
|
||||
return extra;
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Application.Fullscreen" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
</style>
|
||||
</resources>
|
||||
+1363
File diff suppressed because it is too large
Load Diff
+686
@@ -0,0 +1,686 @@
|
||||
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#define NOMINMAX
|
||||
#include "application.hpp"
|
||||
#include "application_events.hpp"
|
||||
#include "application_wsi.hpp"
|
||||
#include "vulkan_headers.hpp"
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include "stb_image_write.h"
|
||||
#include "cli_parser.hpp"
|
||||
#include "os_filesystem.hpp"
|
||||
#include "rapidjson_wrapper.hpp"
|
||||
#include <limits.h>
|
||||
#include <cmath>
|
||||
#include "thread_group.hpp"
|
||||
#include "global_managers_init.hpp"
|
||||
#include "path_utils.hpp"
|
||||
#include "thread_group.hpp"
|
||||
#include "asset_manager.hpp"
|
||||
|
||||
#ifdef HAVE_GRANITE_FFMPEG
|
||||
#include "ffmpeg_encode.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_GRANITE_AUDIO
|
||||
#include "audio_interface.hpp"
|
||||
#include "audio_mixer.hpp"
|
||||
#endif
|
||||
|
||||
using namespace rapidjson;
|
||||
using namespace Vulkan;
|
||||
using namespace Util;
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
struct WSIPlatformHeadless : Granite::GraniteWSIPlatform
|
||||
{
|
||||
public:
|
||||
~WSIPlatformHeadless() override
|
||||
{
|
||||
release_resources();
|
||||
}
|
||||
|
||||
void release_resources() override
|
||||
{
|
||||
for (auto &t : swapchain_tasks)
|
||||
t.reset();
|
||||
if (last_task_dependency)
|
||||
last_task_dependency->wait();
|
||||
last_task_dependency.reset();
|
||||
|
||||
auto *em = GRANITE_EVENT_MANAGER();
|
||||
if (em)
|
||||
{
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||
}
|
||||
|
||||
swapchain_images.clear();
|
||||
readback_buffers.clear();
|
||||
acquire_semaphore.clear();
|
||||
#ifdef HAVE_GRANITE_FFMPEG
|
||||
ycbcr_pipelines.clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool alive(Vulkan::WSI &) override
|
||||
{
|
||||
return frames < max_frames;
|
||||
}
|
||||
|
||||
void poll_input() override
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||
get_input_tracker().dispatch_current_state(get_frame_timer().get_frame_time());
|
||||
}
|
||||
|
||||
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||
get_input_tracker().dispatch_current_state(0.0, override_handler);
|
||||
}
|
||||
|
||||
void enable_png_readback(std::string base_path)
|
||||
{
|
||||
png_readback = std::move(base_path);
|
||||
}
|
||||
|
||||
std::vector<const char *> get_instance_extensions() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
VkSurfaceKHR create_surface(VkInstance, VkPhysicalDevice) override
|
||||
{
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
uint32_t get_surface_width() override
|
||||
{
|
||||
return width;
|
||||
}
|
||||
|
||||
uint32_t get_surface_height() override
|
||||
{
|
||||
return height;
|
||||
}
|
||||
|
||||
void notify_resize(unsigned width_, unsigned height_)
|
||||
{
|
||||
resize = true;
|
||||
width = width_;
|
||||
height = height_;
|
||||
}
|
||||
|
||||
void set_max_frames(unsigned max_frames_)
|
||||
{
|
||||
max_frames = max_frames_;
|
||||
}
|
||||
|
||||
bool has_external_swapchain() override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool init(unsigned width_, unsigned height_)
|
||||
{
|
||||
width = width_;
|
||||
height = height_;
|
||||
if (!Context::init_loader(nullptr))
|
||||
{
|
||||
LOGE("Failed to initialize Vulkan loader.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *em = GRANITE_EVENT_MANAGER();
|
||||
if (em)
|
||||
{
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool init_headless(Application *app_)
|
||||
{
|
||||
app = app_;
|
||||
|
||||
auto context = Util::make_handle<Context>();
|
||||
|
||||
Context::SystemHandles system_handles;
|
||||
system_handles.filesystem = GRANITE_FILESYSTEM();
|
||||
system_handles.thread_group = GRANITE_THREAD_GROUP();
|
||||
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
|
||||
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
|
||||
context->set_system_handles(system_handles);
|
||||
|
||||
context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
|
||||
const char *khr_surface = VK_KHR_SURFACE_EXTENSION_NAME;
|
||||
const char *khr_swapchain = VK_KHR_SWAPCHAIN_EXTENSION_NAME;
|
||||
|
||||
auto name = app->get_name();
|
||||
if (name.empty())
|
||||
name = Path::basename(Path::get_executable_path());
|
||||
VkApplicationInfo app_info = { VK_STRUCTURE_TYPE_APPLICATION_INFO };
|
||||
app_info.pEngineName = "Granite";
|
||||
app_info.pApplicationName = name.empty() ? "Granite" : name.c_str();
|
||||
app_info.apiVersion = VK_API_VERSION_1_1;
|
||||
context->set_application_info(&app_info);
|
||||
|
||||
if (!context->init_instance_and_device(&khr_surface, 1, &khr_swapchain, 1))
|
||||
return false;
|
||||
if (!app->init_wsi(std::move(context)))
|
||||
return false;
|
||||
|
||||
auto &device = app->get_wsi().get_device();
|
||||
|
||||
auto info = ImageCreateInfo::render_target(width, height, VK_FORMAT_R8G8B8A8_SRGB);
|
||||
info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
info.misc |= Vulkan::IMAGE_MISC_MUTABLE_SRGB_BIT;
|
||||
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
|
||||
BufferCreateInfo readback = {};
|
||||
readback.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
readback.domain = BufferDomain::CachedHost;
|
||||
readback.size = width * height * sizeof(uint32_t);
|
||||
|
||||
for (unsigned i = 0; i < SwapchainImages; i++)
|
||||
{
|
||||
swapchain_images.push_back(device.create_image(info, nullptr));
|
||||
readback_buffers.push_back(device.create_buffer(readback, nullptr));
|
||||
acquire_semaphore.emplace_back(nullptr);
|
||||
}
|
||||
|
||||
// Target present layouts to be more accurate for timing in case PRESENT_SRC forces decompress,
|
||||
// and also makes sure pipeline caches are valid w.r.t render passes.
|
||||
for (auto &swap : swapchain_images)
|
||||
swap->set_swapchain_layout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
|
||||
app->get_wsi().init_external_swapchain(swapchain_images);
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef HAVE_GRANITE_FFMPEG
|
||||
void init_headless_recording(std::string path)
|
||||
{
|
||||
#ifndef HAVE_GRANITE_RENDERER
|
||||
LOGE("Need to include system handles in build to encode.\n");
|
||||
return;
|
||||
#endif
|
||||
|
||||
video_encode_path = std::move(path);
|
||||
VideoEncoder::Options enc_opts = {};
|
||||
enc_opts.width = width;
|
||||
enc_opts.height = height;
|
||||
|
||||
double frame_rate = std::round(1.0 / time_step);
|
||||
enc_opts.frame_timebase.num = 1;
|
||||
enc_opts.frame_timebase.den = int(frame_rate);
|
||||
|
||||
#ifdef HAVE_GRANITE_AUDIO
|
||||
enc_opts.walltime_to_pts = true;
|
||||
record_stream.reset(Audio::create_default_audio_record_backend("headless", 44100.0f, 2));
|
||||
if (record_stream)
|
||||
encoder.set_audio_record_stream(record_stream.get());
|
||||
#endif
|
||||
|
||||
if (!encoder.init(&app->get_wsi().get_device(), video_encode_path.c_str(), enc_opts))
|
||||
{
|
||||
LOGE("Failed to initialize encoder.\n");
|
||||
video_encode_path.clear();
|
||||
}
|
||||
|
||||
#ifdef HAVE_GRANITE_RENDERER
|
||||
for (unsigned i = 0; i < SwapchainImages; i++)
|
||||
{
|
||||
auto &device = app->get_wsi().get_device();
|
||||
FFmpegEncode::Shaders<> shaders;
|
||||
|
||||
shaders.scaler = device.get_shader_manager().register_compute(
|
||||
"builtin://shaders/util/scaler.comp")->register_variant({})->get_program();
|
||||
ycbcr_pipelines.push_back(encoder.create_ycbcr_pipeline(shaders));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_GRANITE_AUDIO
|
||||
record_stream->start();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void set_time_step(double t)
|
||||
{
|
||||
time_step = t;
|
||||
}
|
||||
|
||||
void begin_frame()
|
||||
{
|
||||
auto &wsi = app->get_wsi();
|
||||
wsi.set_external_frame(frame_index, std::move(acquire_semaphore[frame_index]), time_step);
|
||||
acquire_semaphore[frame_index] = {};
|
||||
}
|
||||
|
||||
void end_frame()
|
||||
{
|
||||
auto &wsi = app->get_wsi();
|
||||
auto &device = wsi.get_device();
|
||||
auto release_semaphore = wsi.consume_external_release_semaphore();
|
||||
|
||||
if (release_semaphore && release_semaphore->get_semaphore() != VK_NULL_HANDLE)
|
||||
{
|
||||
if (swapchain_tasks[frame_index])
|
||||
{
|
||||
swapchain_tasks[frame_index]->wait();
|
||||
swapchain_tasks[frame_index].reset();
|
||||
}
|
||||
|
||||
acquire_semaphore[frame_index] = {};
|
||||
|
||||
if (!next_readback_path.empty() || !png_readback.empty())
|
||||
{
|
||||
OwnershipTransferInfo transfer_info = {};
|
||||
transfer_info.old_queue = wsi.get_current_present_queue_type();
|
||||
transfer_info.new_queue = CommandBuffer::Type::AsyncTransfer;
|
||||
transfer_info.old_image_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
transfer_info.new_image_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
transfer_info.dst_pipeline_stage = VK_PIPELINE_STAGE_2_COPY_BIT;
|
||||
transfer_info.dst_access = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
auto cmd = request_command_buffer_with_ownership_transfer(device, *swapchain_images[frame_index],
|
||||
transfer_info, release_semaphore);
|
||||
|
||||
cmd->copy_image_to_buffer(*readback_buffers[frame_index], *swapchain_images[frame_index],
|
||||
0, {}, {width, height, 1},
|
||||
0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1});
|
||||
|
||||
cmd->barrier(VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
|
||||
Fence readback_fence;
|
||||
device.submit(cmd, &readback_fence, 1, &acquire_semaphore[frame_index]);
|
||||
|
||||
if (!next_readback_path.empty())
|
||||
{
|
||||
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
|
||||
[this, readback_fence, index = frame_index, frame = this->frames, p = std::make_unique<std::string>(next_readback_path)]() mutable {
|
||||
readback_fence->wait();
|
||||
dump_frame_single(*p, frame, index);
|
||||
});
|
||||
next_readback_path.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
|
||||
[this, readback_fence, index = frame_index, frame = this->frames]() mutable {
|
||||
readback_fence->wait();
|
||||
dump_frame(frame, index);
|
||||
});
|
||||
}
|
||||
}
|
||||
#ifdef HAVE_GRANITE_FFMPEG
|
||||
else if (!video_encode_path.empty())
|
||||
{
|
||||
auto pts = encoder.sample_realtime_pts();
|
||||
|
||||
OwnershipTransferInfo transfer_info = {};
|
||||
transfer_info.old_queue = wsi.get_current_present_queue_type();
|
||||
transfer_info.new_queue = CommandBuffer::Type::AsyncCompute;
|
||||
transfer_info.old_image_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
transfer_info.new_image_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
|
||||
transfer_info.dst_pipeline_stage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
transfer_info.dst_access = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
|
||||
auto cmd = request_command_buffer_with_ownership_transfer(device, *swapchain_images[frame_index],
|
||||
transfer_info, release_semaphore);
|
||||
|
||||
encoder.process_rgb(*cmd, ycbcr_pipelines[frame_index], swapchain_images[frame_index]->get_view());
|
||||
encoder.submit_process_rgb(cmd, ycbcr_pipelines[frame_index]);
|
||||
|
||||
acquire_semaphore[frame_index] = device.request_semaphore(VK_SEMAPHORE_TYPE_BINARY);
|
||||
device.submit_empty(CommandBuffer::Type::AsyncCompute, nullptr, acquire_semaphore[frame_index].get());
|
||||
|
||||
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
|
||||
[this, index = frame_index, pts]() mutable {
|
||||
if (!encoder.encode_frame(ycbcr_pipelines[index], pts))
|
||||
LOGE("Failed to push frame to encoder.\n");
|
||||
});
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
// Do nothing.
|
||||
acquire_semaphore[frame_index] = std::move(release_semaphore);
|
||||
}
|
||||
|
||||
if (swapchain_tasks[frame_index])
|
||||
{
|
||||
swapchain_tasks[frame_index]->set_desc("application-headless-readback");
|
||||
swapchain_tasks[frame_index]->set_task_class(TaskClass::Background);
|
||||
|
||||
if (last_task_dependency)
|
||||
GRANITE_THREAD_GROUP()->add_dependency(*swapchain_tasks[frame_index], *last_task_dependency);
|
||||
|
||||
// Add a dummy task that only serves to chain dependencies.
|
||||
last_task_dependency = GRANITE_THREAD_GROUP()->create_task();
|
||||
last_task_dependency->set_task_class(TaskClass::Background);
|
||||
GRANITE_THREAD_GROUP()->add_dependency(*last_task_dependency, *swapchain_tasks[frame_index]);
|
||||
swapchain_tasks[frame_index]->flush();
|
||||
}
|
||||
}
|
||||
|
||||
release_semaphore = {};
|
||||
frame_index = (frame_index + 1) % SwapchainImages;
|
||||
frames++;
|
||||
}
|
||||
|
||||
void set_next_readback(std::string path)
|
||||
{
|
||||
next_readback_path = std::move(path);
|
||||
}
|
||||
|
||||
void wait_threads()
|
||||
{
|
||||
GRANITE_THREAD_GROUP()->wait_idle();
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
unsigned frames = 0;
|
||||
unsigned max_frames = UINT_MAX;
|
||||
unsigned frame_index = 0;
|
||||
double time_step = 0.01;
|
||||
std::string png_readback;
|
||||
std::string video_encode_path;
|
||||
enum { SwapchainImages = 4 };
|
||||
|
||||
#ifdef HAVE_GRANITE_AUDIO
|
||||
std::unique_ptr<Audio::RecordStream> record_stream;
|
||||
#endif
|
||||
|
||||
std::vector<ImageHandle> swapchain_images;
|
||||
std::vector<BufferHandle> readback_buffers;
|
||||
std::vector<Semaphore> acquire_semaphore;
|
||||
std::string next_readback_path;
|
||||
TaskGroupHandle swapchain_tasks[SwapchainImages];
|
||||
TaskGroupHandle last_task_dependency;
|
||||
|
||||
#ifdef HAVE_GRANITE_FFMPEG
|
||||
VideoEncoder encoder;
|
||||
std::vector<VideoEncoder::YCbCrPipeline> ycbcr_pipelines;
|
||||
#endif
|
||||
|
||||
void dump_frame_single(const std::string &path, unsigned frame, unsigned index)
|
||||
{
|
||||
auto &wsi = app->get_wsi();
|
||||
auto &device = wsi.get_device();
|
||||
|
||||
LOGI("Dumping frame: %u (index: %u)\n", frame, index);
|
||||
|
||||
auto *ptr = static_cast<uint32_t *>(device.map_host_buffer(*readback_buffers[index], MEMORY_ACCESS_READ_WRITE_BIT));
|
||||
for (unsigned i = 0; i < width * height; i++)
|
||||
ptr[i] |= 0xff000000u;
|
||||
|
||||
if (!stbi_write_png(path.c_str(), width, height, 4, ptr, width * 4))
|
||||
LOGE("Failed to write PNG to disk.\n");
|
||||
device.unmap_host_buffer(*readback_buffers[index], MEMORY_ACCESS_READ_WRITE_BIT);
|
||||
}
|
||||
|
||||
void dump_frame(unsigned frame, unsigned index)
|
||||
{
|
||||
char buffer[64];
|
||||
sprintf(buffer, "_%05u.png", frame);
|
||||
auto path = png_readback + buffer;
|
||||
dump_frame_single(path, frame, index);
|
||||
}
|
||||
|
||||
Application *app = nullptr;
|
||||
};
|
||||
}
|
||||
|
||||
static void print_help()
|
||||
{
|
||||
LOGI("[--png-path <path>] [--stat <output.json>]\n"
|
||||
"[--fs-assets <path>] [--fs-cache <path>] [--fs-builtin <path>]\n"
|
||||
"[--video-encode-path <path>]\n"
|
||||
"[--png-reference-path <path>] [--frames <frames>] [--width <width>] [--height <height>] [--time-step <step>].\n");
|
||||
}
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
int application_main_headless(
|
||||
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
|
||||
Application *(*create_application)(int, char **),
|
||||
int argc, char *argv[])
|
||||
{
|
||||
if (argc < 1)
|
||||
return 1;
|
||||
|
||||
struct Args
|
||||
{
|
||||
std::string png_path;
|
||||
std::string video_encode_path;
|
||||
std::string png_reference_path;
|
||||
std::string stat;
|
||||
std::string assets;
|
||||
std::string cache;
|
||||
std::string builtin;
|
||||
unsigned max_frames = UINT_MAX;
|
||||
unsigned width = 1280;
|
||||
unsigned height = 720;
|
||||
double time_step = 0.01;
|
||||
} args;
|
||||
|
||||
CLICallbacks cbs;
|
||||
cbs.add("--frames", [&](CLIParser &parser) { args.max_frames = parser.next_uint(); });
|
||||
cbs.add("--width", [&](CLIParser &parser) { args.width = parser.next_uint(); });
|
||||
cbs.add("--height", [&](CLIParser &parser) { args.height = parser.next_uint(); });
|
||||
cbs.add("--time-step", [&](CLIParser &parser) { args.time_step = parser.next_double(); });
|
||||
cbs.add("--png-path", [&](CLIParser &parser) { args.png_path = parser.next_string(); });
|
||||
cbs.add("--png-reference-path", [&](CLIParser &parser) { args.png_reference_path = parser.next_string(); });
|
||||
cbs.add("--video-encode-path", [&](CLIParser &parser) { args.video_encode_path = parser.next_string(); });
|
||||
cbs.add("--fs-assets", [&](CLIParser &parser) { args.assets = parser.next_string(); });
|
||||
cbs.add("--fs-builtin", [&](CLIParser &parser) { args.builtin = parser.next_string(); });
|
||||
cbs.add("--fs-cache", [&](CLIParser &parser) { args.cache = parser.next_string(); });
|
||||
cbs.add("--stat", [&](CLIParser &parser) { args.stat = parser.next_string(); });
|
||||
cbs.add("--help", [](CLIParser &parser)
|
||||
{
|
||||
print_help();
|
||||
parser.end();
|
||||
});
|
||||
cbs.error_handler = [&]() { print_help(); };
|
||||
int exit_code;
|
||||
|
||||
if (!Util::parse_cli_filtered(std::move(cbs), argc, argv, exit_code))
|
||||
return exit_code;
|
||||
|
||||
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
|
||||
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
|
||||
Granite::Global::init(flags.manager_feature_flags);
|
||||
|
||||
if (flags.manager_feature_flags & Global::MANAGER_FEATURE_FILESYSTEM_BIT)
|
||||
{
|
||||
if (!args.assets.empty())
|
||||
GRANITE_FILESYSTEM()->register_protocol("assets", std::make_unique<OSFilesystem>(args.assets));
|
||||
if (!args.builtin.empty())
|
||||
GRANITE_FILESYSTEM()->register_protocol("builtin", std::make_unique<OSFilesystem>(args.builtin));
|
||||
if (!args.cache.empty())
|
||||
GRANITE_FILESYSTEM()->register_protocol("cache", std::make_unique<OSFilesystem>(args.cache));
|
||||
}
|
||||
|
||||
auto app = std::unique_ptr<Application>(create_application(argc, argv));
|
||||
|
||||
if (app)
|
||||
{
|
||||
auto platform = std::make_unique<WSIPlatformHeadless>();
|
||||
if (!platform->init(args.width, args.height))
|
||||
return 1;
|
||||
|
||||
auto *p = platform.get();
|
||||
|
||||
if (!app->init_platform(std::move(platform)))
|
||||
return 1;
|
||||
|
||||
p->set_max_frames(args.max_frames);
|
||||
p->set_time_step(args.time_step);
|
||||
p->init_headless(app.get());
|
||||
|
||||
// Ensure all startup work is complete.
|
||||
while (app->get_wsi().get_device().query_initialization_progress(Vulkan::Device::InitializationStage::Pipelines) < 100 &&
|
||||
app->poll())
|
||||
{
|
||||
p->begin_frame();
|
||||
app->run_frame();
|
||||
p->end_frame();
|
||||
}
|
||||
|
||||
if (!args.png_path.empty())
|
||||
p->enable_png_readback(args.png_path);
|
||||
|
||||
if (!args.video_encode_path.empty())
|
||||
{
|
||||
#ifdef HAVE_GRANITE_FFMPEG
|
||||
p->init_headless_recording(args.video_encode_path);
|
||||
#else
|
||||
LOGE("FFmpeg is not enabled in build.\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef HAVE_GRANITE_AUDIO
|
||||
Global::start_audio_system();
|
||||
#endif
|
||||
|
||||
// Run warm-up frame.
|
||||
if (app->poll())
|
||||
{
|
||||
p->begin_frame();
|
||||
app->run_frame();
|
||||
p->end_frame();
|
||||
}
|
||||
|
||||
p->wait_threads();
|
||||
app->get_wsi().get_device().wait_idle();
|
||||
app->get_wsi().get_device().timestamp_log_reset();
|
||||
|
||||
LOGI("=== Begin run ===\n");
|
||||
|
||||
auto start_time = get_current_time_nsecs();
|
||||
unsigned rendered_frames = 0;
|
||||
while (app->poll())
|
||||
{
|
||||
p->begin_frame();
|
||||
app->run_frame();
|
||||
p->end_frame();
|
||||
if (!args.video_encode_path.empty() || !args.png_path.empty())
|
||||
{
|
||||
LOGI(" Queued frame %u (Total time = %.3f ms).\n", rendered_frames,
|
||||
1e-6 * double(get_current_time_nsecs() - start_time));
|
||||
}
|
||||
rendered_frames++;
|
||||
}
|
||||
|
||||
p->wait_threads();
|
||||
app->get_wsi().get_device().wait_idle();
|
||||
auto end_time = get_current_time_nsecs();
|
||||
|
||||
LOGI("=== End run ===\n");
|
||||
|
||||
struct Report
|
||||
{
|
||||
std::string tag;
|
||||
TimestampIntervalReport report;
|
||||
};
|
||||
std::vector<Report> reports;
|
||||
app->get_wsi().get_device().timestamp_log([&](const std::string &tag, const TimestampIntervalReport &report) {
|
||||
reports.push_back({ tag, report });
|
||||
});
|
||||
app->get_wsi().get_device().timestamp_log_reset();
|
||||
|
||||
if (rendered_frames)
|
||||
{
|
||||
double usec = 1e-3 * double(end_time - start_time) / rendered_frames;
|
||||
LOGI("Average frame time: %.3f usec\n", usec);
|
||||
|
||||
if (!args.stat.empty())
|
||||
{
|
||||
Document doc;
|
||||
doc.SetObject();
|
||||
auto &allocator = doc.GetAllocator();
|
||||
|
||||
doc.AddMember("averageFrameTimeUs", usec, allocator);
|
||||
doc.AddMember("gpu", StringRef(app->get_wsi().get_context().get_gpu_props().deviceName), allocator);
|
||||
doc.AddMember("driverVersion", app->get_wsi().get_context().get_gpu_props().driverVersion, allocator);
|
||||
|
||||
if (!reports.empty())
|
||||
{
|
||||
Value report_objs(kObjectType);
|
||||
for (auto &rep : reports)
|
||||
{
|
||||
Value report_obj(kObjectType);
|
||||
report_obj.AddMember("timePerAccumulationUs", 1e6 * rep.report.time_per_accumulation, allocator);
|
||||
report_obj.AddMember("timePerFrameContextUs", 1e6 * rep.report.time_per_frame_context, allocator);
|
||||
report_obj.AddMember("accumulationsPerFrameContext", rep.report.accumulations_per_frame_context, allocator);
|
||||
report_objs.AddMember(StringRef(rep.tag), report_obj, allocator);
|
||||
}
|
||||
doc.AddMember("performance", report_objs, allocator);
|
||||
}
|
||||
|
||||
StringBuffer buffer;
|
||||
PrettyWriter<StringBuffer> writer(buffer);
|
||||
doc.Accept(writer);
|
||||
|
||||
if (!GRANITE_FILESYSTEM()->write_string_to_file(args.stat, buffer.GetString()))
|
||||
LOGE("Failed to write stat file to disk.\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.png_reference_path.empty())
|
||||
{
|
||||
p->set_next_readback(args.png_reference_path);
|
||||
p->begin_frame();
|
||||
app->run_frame();
|
||||
p->end_frame();
|
||||
}
|
||||
|
||||
p->wait_threads();
|
||||
|
||||
#ifdef HAVE_GRANITE_AUDIO
|
||||
Global::stop_audio_system();
|
||||
#endif
|
||||
|
||||
app.reset();
|
||||
Granite::Global::deinit();
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "application_glue.hpp"
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
int application_main(
|
||||
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
|
||||
Application *(*create_application)(int, char **), int argc, char *argv[])
|
||||
{
|
||||
return application_main_headless(query_application_interface, create_application, argc, argv);
|
||||
}
|
||||
}
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "application_libretro_utils.hpp"
|
||||
#include "global_managers_init.hpp"
|
||||
#include "application.hpp"
|
||||
#include "application_wsi.hpp"
|
||||
#include "muglm/muglm_impl.hpp"
|
||||
|
||||
using namespace Granite;
|
||||
|
||||
static Application *app;
|
||||
static retro_environment_t environ_cb;
|
||||
static retro_video_refresh_t video_cb;
|
||||
static retro_audio_sample_batch_t audio_cb;
|
||||
static retro_input_poll_t input_poll_cb;
|
||||
static retro_input_state_t input_state_cb;
|
||||
|
||||
static retro_usec_t last_frame_time;
|
||||
static std::string application_name;
|
||||
static std::string application_internal_resolution;
|
||||
|
||||
static unsigned current_width;
|
||||
static unsigned current_height;
|
||||
|
||||
struct WSIPlatformLibretro : Granite::GraniteWSIPlatform
|
||||
{
|
||||
VkSurfaceKHR create_surface(VkInstance, VkPhysicalDevice) override
|
||||
{
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
std::vector<const char *> get_instance_extensions() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
unsigned get_surface_width() override
|
||||
{
|
||||
return current_width;
|
||||
}
|
||||
|
||||
unsigned get_surface_height() override
|
||||
{
|
||||
return current_height;
|
||||
}
|
||||
|
||||
bool alive(Vulkan::WSI &) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void poll_input() override
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||
input_poll_cb();
|
||||
|
||||
auto &tracker = get_input_tracker();
|
||||
const auto poll_key = [&](unsigned index, JoypadKey key, unsigned retro_key) {
|
||||
tracker.joypad_key_state(index, key,
|
||||
input_state_cb(index, RETRO_DEVICE_JOYPAD, 0, retro_key)
|
||||
? JoypadKeyState::Pressed : JoypadKeyState::Released);
|
||||
};
|
||||
|
||||
const auto poll_axis = [&](unsigned index, JoypadAxis axis, unsigned retro_index, unsigned retro_id) {
|
||||
tracker.joyaxis_state(index, axis,
|
||||
clamp(input_state_cb(index, RETRO_DEVICE_ANALOG,
|
||||
retro_index, retro_id) * (1.0f / 0x7fff), -1.0f, 1.0f));
|
||||
};
|
||||
|
||||
const auto poll_axis_button = [&](unsigned index, JoypadAxis axis, unsigned retro_key) {
|
||||
tracker.joyaxis_state(index, axis,
|
||||
input_state_cb(index, RETRO_DEVICE_JOYPAD, 0, retro_key) ? 1.0f : 0.0f);
|
||||
};
|
||||
|
||||
tracker.enable_joypad(0, 0, 0);
|
||||
tracker.enable_joypad(1, 0, 0);
|
||||
for (unsigned i = 0; i < 2; i++)
|
||||
{
|
||||
poll_key(i, JoypadKey::Left, RETRO_DEVICE_ID_JOYPAD_LEFT);
|
||||
poll_key(i, JoypadKey::Right, RETRO_DEVICE_ID_JOYPAD_RIGHT);
|
||||
poll_key(i, JoypadKey::Up, RETRO_DEVICE_ID_JOYPAD_UP);
|
||||
poll_key(i, JoypadKey::Down, RETRO_DEVICE_ID_JOYPAD_DOWN);
|
||||
poll_key(i, JoypadKey::Select, RETRO_DEVICE_ID_JOYPAD_SELECT);
|
||||
poll_key(i, JoypadKey::Start, RETRO_DEVICE_ID_JOYPAD_START);
|
||||
poll_key(i, JoypadKey::LeftShoulder, RETRO_DEVICE_ID_JOYPAD_L);
|
||||
poll_key(i, JoypadKey::LeftThumb, RETRO_DEVICE_ID_JOYPAD_L3);
|
||||
poll_key(i, JoypadKey::RightShoulder, RETRO_DEVICE_ID_JOYPAD_R);
|
||||
poll_key(i, JoypadKey::RightThumb, RETRO_DEVICE_ID_JOYPAD_R3);
|
||||
poll_key(i, JoypadKey::South, RETRO_DEVICE_ID_JOYPAD_B);
|
||||
poll_key(i, JoypadKey::East, RETRO_DEVICE_ID_JOYPAD_A);
|
||||
poll_key(i, JoypadKey::North, RETRO_DEVICE_ID_JOYPAD_X);
|
||||
poll_key(i, JoypadKey::West, RETRO_DEVICE_ID_JOYPAD_Y);
|
||||
|
||||
poll_axis(i, JoypadAxis::LeftX, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X);
|
||||
poll_axis(i, JoypadAxis::LeftY, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y);
|
||||
poll_axis(i, JoypadAxis::RightX, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X);
|
||||
poll_axis(i, JoypadAxis::RightY, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y);
|
||||
|
||||
poll_axis_button(i, JoypadAxis::LeftTrigger, RETRO_DEVICE_ID_JOYPAD_L2);
|
||||
poll_axis_button(i, JoypadAxis::RightTrigger, RETRO_DEVICE_ID_JOYPAD_R2);
|
||||
}
|
||||
|
||||
tracker.dispatch_current_state(app->get_platform().get_frame_timer().get_frame_time());
|
||||
}
|
||||
|
||||
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||
get_input_tracker().dispatch_current_state(0.0, override_handler);
|
||||
}
|
||||
|
||||
bool has_external_swapchain() override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static retro_hw_render_callback hw_render;
|
||||
|
||||
RETRO_API void retro_init(void)
|
||||
{
|
||||
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
|
||||
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
|
||||
Global::init(flags.manager_feature_flags);
|
||||
}
|
||||
|
||||
RETRO_API void retro_deinit(void)
|
||||
{
|
||||
Global::deinit();
|
||||
}
|
||||
|
||||
static void setup_variables()
|
||||
{
|
||||
application_internal_resolution = application_name + "_internal_resolution";
|
||||
|
||||
static const retro_variable variables[] = {
|
||||
{ application_internal_resolution.c_str(), "Internal resolution; 1280x720|640x360|1280x1024|1920x1080" },
|
||||
{ nullptr, nullptr },
|
||||
};
|
||||
environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, const_cast<retro_variable *>(variables));
|
||||
}
|
||||
|
||||
static void query_variables()
|
||||
{
|
||||
retro_variable var = { application_internal_resolution.c_str(), nullptr };
|
||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
||||
{
|
||||
unsigned new_width, new_height;
|
||||
if (sscanf(var.value, "%ux%u", &new_width, &new_height) == 2)
|
||||
{
|
||||
current_width = new_width;
|
||||
current_height = new_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RETRO_API void retro_set_environment(retro_environment_t cb)
|
||||
{
|
||||
environ_cb = cb;
|
||||
|
||||
retro_log_callback log_interface;
|
||||
if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log_interface))
|
||||
Granite::libretro_log = log_interface.log;
|
||||
}
|
||||
|
||||
RETRO_API void retro_set_video_refresh(retro_video_refresh_t cb)
|
||||
{
|
||||
video_cb = cb;
|
||||
}
|
||||
|
||||
RETRO_API void retro_set_audio_sample(retro_audio_sample_t)
|
||||
{
|
||||
}
|
||||
|
||||
RETRO_API void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb)
|
||||
{
|
||||
audio_cb = cb;
|
||||
}
|
||||
|
||||
RETRO_API void retro_set_input_poll(retro_input_poll_t cb)
|
||||
{
|
||||
input_poll_cb = cb;
|
||||
}
|
||||
|
||||
RETRO_API void retro_set_input_state(retro_input_state_t cb)
|
||||
{
|
||||
input_state_cb = cb;
|
||||
}
|
||||
|
||||
RETRO_API unsigned retro_api_version(void)
|
||||
{
|
||||
return RETRO_API_VERSION;
|
||||
}
|
||||
|
||||
RETRO_API void retro_get_system_info(struct retro_system_info *info)
|
||||
{
|
||||
info->block_extract = false;
|
||||
info->library_name = "Sample Scene Viewer";
|
||||
info->library_version = "0.0";
|
||||
info->need_fullpath = true;
|
||||
info->valid_extensions = "gltf|glb|scene";
|
||||
}
|
||||
|
||||
RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info)
|
||||
{
|
||||
info->timing.fps = 60.0;
|
||||
info->timing.sample_rate = 44100.0;
|
||||
info->geometry.aspect_ratio = float(current_width) / current_height;
|
||||
info->geometry.base_height = current_width;
|
||||
info->geometry.base_width = current_height;
|
||||
info->geometry.max_width = current_width;
|
||||
info->geometry.max_height = current_height;
|
||||
}
|
||||
|
||||
RETRO_API void retro_set_controller_port_device(unsigned, unsigned)
|
||||
{
|
||||
}
|
||||
|
||||
RETRO_API void retro_reset(void)
|
||||
{
|
||||
}
|
||||
|
||||
static void check_variables()
|
||||
{
|
||||
bool updated = false;
|
||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated)
|
||||
{
|
||||
unsigned old_width = current_width;
|
||||
unsigned old_height = current_height;
|
||||
query_variables();
|
||||
if (old_width != current_width || old_height != current_height)
|
||||
{
|
||||
retro_system_av_info av_info;
|
||||
retro_get_system_av_info(&av_info);
|
||||
libretro_set_swapchain_size(current_width, current_height);
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &av_info))
|
||||
{
|
||||
current_width = old_width;
|
||||
current_height = old_height;
|
||||
libretro_set_swapchain_size(current_width, current_height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RETRO_API void retro_run(void)
|
||||
{
|
||||
if (!app)
|
||||
{
|
||||
// The application is dead, force a shutdown.
|
||||
input_poll_cb();
|
||||
environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
check_variables();
|
||||
|
||||
// Begin frame.
|
||||
libretro_begin_frame(app->get_wsi(), last_frame_time);
|
||||
|
||||
// Run frame.
|
||||
app->poll();
|
||||
app->run_frame();
|
||||
|
||||
// Present.
|
||||
libretro_end_frame(video_cb, app->get_wsi());
|
||||
}
|
||||
|
||||
RETRO_API size_t retro_serialize_size(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
RETRO_API bool retro_serialize(void *, size_t)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RETRO_API bool retro_unserialize(const void *, size_t)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RETRO_API void retro_cheat_reset(void)
|
||||
{
|
||||
}
|
||||
|
||||
RETRO_API void retro_cheat_set(unsigned, bool, const char *)
|
||||
{
|
||||
}
|
||||
|
||||
static void context_destroy(void)
|
||||
{
|
||||
libretro_context_destroy(app);
|
||||
}
|
||||
|
||||
static void context_reset(void)
|
||||
{
|
||||
retro_hw_render_interface_vulkan *vulkan;
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE, &vulkan))
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_ERROR, "Didn't get Vulkan HW interface.");
|
||||
delete app;
|
||||
app = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!libretro_context_reset(vulkan, *app))
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to reset Vulkan context.");
|
||||
delete app;
|
||||
app = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void frame_time_callback(retro_usec_t usecs)
|
||||
{
|
||||
last_frame_time = usecs;
|
||||
}
|
||||
|
||||
RETRO_API bool retro_load_game(const struct retro_game_info *info)
|
||||
{
|
||||
char *argv[] = {
|
||||
const_cast<char *>("libretro-granite"),
|
||||
const_cast<char *>(info->path),
|
||||
nullptr,
|
||||
};
|
||||
|
||||
app = Granite::application_create(2, argv);
|
||||
if (!app)
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to load scene: %s\n", info->path);
|
||||
return false;
|
||||
}
|
||||
|
||||
current_width = app->get_default_width();
|
||||
current_height = app->get_default_height();
|
||||
|
||||
if (!app->init_platform(std::make_unique<WSIPlatformLibretro>()))
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to init platform.");
|
||||
delete app;
|
||||
return false;
|
||||
}
|
||||
|
||||
application_name = app->get_name();
|
||||
libretro_set_application_info(application_name.c_str(), app->get_version());
|
||||
|
||||
setup_variables();
|
||||
query_variables();
|
||||
libretro_set_swapchain_size(current_width, current_height);
|
||||
|
||||
hw_render.context_destroy = context_destroy;
|
||||
hw_render.context_reset = context_reset;
|
||||
hw_render.context_type = RETRO_HW_CONTEXT_VULKAN;
|
||||
hw_render.version_major = 1;
|
||||
hw_render.version_minor = 0;
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render))
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_ERROR, "SET_HW_RENDER failed, this core cannot run.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!libretro_load_game(environ_cb))
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to set up Vulkan application, this core cannot run.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
retro_frame_time_callback frame_cb = {};
|
||||
frame_cb.callback = frame_time_callback;
|
||||
frame_cb.reference = (1000000 + 30) / 60;
|
||||
last_frame_time = frame_cb.reference;
|
||||
environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, &frame_cb);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
RETRO_API bool retro_load_game_special(unsigned, const struct retro_game_info *, size_t)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RETRO_API void retro_unload_game(void)
|
||||
{
|
||||
libretro_unload_game();
|
||||
delete app;
|
||||
app = nullptr;
|
||||
}
|
||||
|
||||
RETRO_API unsigned retro_get_region(void)
|
||||
{
|
||||
return RETRO_REGION_NTSC;
|
||||
}
|
||||
|
||||
RETRO_API void *retro_get_memory_data(unsigned)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RETRO_API size_t retro_get_memory_size(unsigned)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
void application_dummy()
|
||||
{
|
||||
}
|
||||
|
||||
// Alternatively, make sure this is linked in.
|
||||
// Implementation is here to trick a linker to always let main() in static library work.
|
||||
void application_setup_default_filesystem(const char *default_asset_directory)
|
||||
{
|
||||
auto *filesystem = GRANITE_FILESYSTEM();
|
||||
if (filesystem)
|
||||
Filesystem::setup_default_filesystem(filesystem, default_asset_directory);
|
||||
}
|
||||
}
|
||||
Vendored
+403
@@ -0,0 +1,403 @@
|
||||
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "application_libretro_utils.hpp"
|
||||
#include "application.hpp"
|
||||
#include "application_events.hpp"
|
||||
#include "thread_group.hpp"
|
||||
#include "asset_manager.hpp"
|
||||
#include "context.hpp"
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
retro_log_printf_t libretro_log;
|
||||
static retro_hw_render_interface_vulkan *vulkan_interface;
|
||||
static retro_hw_render_context_negotiation_interface_vulkan vulkan_negotiation;
|
||||
static Vulkan::ContextHandle vulkan_context;
|
||||
static Vulkan::ImageViewHandle swapchain_unorm_view;
|
||||
static Vulkan::ImageHandle swapchain_image;
|
||||
static retro_vulkan_image swapchain_image_info;
|
||||
static bool can_dupe = false;
|
||||
static std::string application_name;
|
||||
static unsigned application_version;
|
||||
|
||||
static unsigned swapchain_width;
|
||||
static unsigned swapchain_height;
|
||||
static unsigned swapchain_frame_index;
|
||||
static Vulkan::Semaphore acquire_semaphore;
|
||||
|
||||
static VkApplicationInfo vulkan_app = {
|
||||
VK_STRUCTURE_TYPE_APPLICATION_INFO,
|
||||
nullptr,
|
||||
nullptr, 0,
|
||||
"Granite",
|
||||
0,
|
||||
VK_API_VERSION_1_1,
|
||||
};
|
||||
|
||||
void libretro_set_swapchain_size(unsigned width, unsigned height)
|
||||
{
|
||||
swapchain_width = width;
|
||||
swapchain_height = height;
|
||||
}
|
||||
|
||||
void libretro_set_application_info(const char *name, unsigned version)
|
||||
{
|
||||
application_name = name;
|
||||
application_version = version;
|
||||
vulkan_app.pApplicationName = application_name.c_str();
|
||||
vulkan_app.applicationVersion = application_version;
|
||||
}
|
||||
|
||||
bool libretro_create_device(
|
||||
struct retro_vulkan_context *context,
|
||||
VkInstance instance,
|
||||
VkPhysicalDevice gpu,
|
||||
VkSurfaceKHR surface,
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
const char **required_device_extensions,
|
||||
unsigned num_required_device_extensions,
|
||||
const char **, unsigned, // Deprecated.
|
||||
const VkPhysicalDeviceFeatures *required_features)
|
||||
{
|
||||
if (!Vulkan::Context::init_loader(get_instance_proc_addr))
|
||||
return false;
|
||||
|
||||
vulkan_context = Util::make_handle<Vulkan::Context>();
|
||||
Vulkan::Context::SystemHandles system_handles;
|
||||
system_handles.filesystem = GRANITE_FILESYSTEM();
|
||||
system_handles.thread_group = GRANITE_THREAD_GROUP();
|
||||
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
|
||||
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
|
||||
vulkan_context->set_system_handles(system_handles);
|
||||
vulkan_context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
|
||||
if (!vulkan_context->init_device_from_instance(instance, gpu, surface, required_device_extensions, num_required_device_extensions,
|
||||
required_features))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
vulkan_context->release_device();
|
||||
context->gpu = vulkan_context->get_gpu();
|
||||
context->device = vulkan_context->get_device();
|
||||
context->presentation_queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||
context->presentation_queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||
context->queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||
context->queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||
return true;
|
||||
}
|
||||
|
||||
static VkInstance libretro_create_instance(
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
const VkApplicationInfo *app,
|
||||
retro_vulkan_create_instance_wrapper_t create_instance_wrapper,
|
||||
void *opaque)
|
||||
{
|
||||
if (!Vulkan::Context::init_loader(get_instance_proc_addr))
|
||||
return VK_NULL_HANDLE;
|
||||
|
||||
vulkan_context = Util::make_handle<Vulkan::Context>();
|
||||
Vulkan::Context::SystemHandles system_handles;
|
||||
system_handles.filesystem = GRANITE_FILESYSTEM();
|
||||
system_handles.thread_group = GRANITE_THREAD_GROUP();
|
||||
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
|
||||
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
|
||||
|
||||
vulkan_context->set_application_info(app);
|
||||
vulkan_context->set_system_handles(system_handles);
|
||||
vulkan_context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
|
||||
|
||||
struct Factory final : Vulkan::InstanceFactory
|
||||
{
|
||||
VkInstance create_instance(const VkInstanceCreateInfo *info) override
|
||||
{
|
||||
return wrapper(opaque, info);
|
||||
}
|
||||
|
||||
retro_vulkan_create_instance_wrapper_t wrapper = nullptr;
|
||||
void *opaque = nullptr;
|
||||
} factory;
|
||||
|
||||
factory.wrapper = create_instance_wrapper;
|
||||
factory.opaque = opaque;
|
||||
vulkan_context->set_instance_factory(&factory);
|
||||
|
||||
if (!vulkan_context->init_instance(nullptr, 0))
|
||||
{
|
||||
vulkan_context.reset();
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
vulkan_context->release_instance();
|
||||
return vulkan_context->get_instance();
|
||||
}
|
||||
|
||||
static bool libretro_create_device2(
|
||||
struct retro_vulkan_context *context,
|
||||
VkInstance instance,
|
||||
VkPhysicalDevice gpu,
|
||||
VkSurfaceKHR surface,
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
retro_vulkan_create_device_wrapper_t create_device_wrapper,
|
||||
void *opaque)
|
||||
{
|
||||
// We are guaranteed that create_instance has been called here.
|
||||
if (!vulkan_context)
|
||||
return false;
|
||||
|
||||
// Sanity check inputs.
|
||||
if (vulkan_context->get_instance() != instance)
|
||||
return false;
|
||||
if (Vulkan::Context::get_instance_proc_addr() != get_instance_proc_addr)
|
||||
return false;
|
||||
|
||||
struct Factory final : Vulkan::DeviceFactory
|
||||
{
|
||||
VkDevice create_device(VkPhysicalDevice gpu, const VkDeviceCreateInfo *info) override
|
||||
{
|
||||
return wrapper(gpu, opaque, info);
|
||||
}
|
||||
|
||||
retro_vulkan_create_device_wrapper_t wrapper = nullptr;
|
||||
void *opaque = nullptr;
|
||||
} factory;
|
||||
|
||||
factory.wrapper = create_device_wrapper;
|
||||
factory.opaque = opaque;
|
||||
vulkan_context->set_device_factory(&factory);
|
||||
|
||||
if (!vulkan_context->init_device(gpu, surface, nullptr, 0))
|
||||
return false;
|
||||
|
||||
vulkan_context->release_device();
|
||||
context->gpu = vulkan_context->get_gpu();
|
||||
context->device = vulkan_context->get_device();
|
||||
context->presentation_queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||
context->presentation_queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||
context->queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||
context->queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||
return true;
|
||||
}
|
||||
|
||||
void libretro_begin_frame(Vulkan::WSI &wsi, retro_usec_t frame_time)
|
||||
{
|
||||
// Setup the external frame.
|
||||
vulkan_interface->wait_sync_index(vulkan_interface->handle);
|
||||
wsi.set_external_frame(swapchain_frame_index, std::move(acquire_semaphore), double(frame_time) * 1e-6);
|
||||
acquire_semaphore = {};
|
||||
|
||||
swapchain_frame_index ^= 1;
|
||||
}
|
||||
|
||||
void libretro_end_frame(retro_video_refresh_t video_cb, Vulkan::WSI &wsi)
|
||||
{
|
||||
// Present to libretro frontend.
|
||||
auto signal_semaphore = wsi.get_device().request_semaphore(VK_SEMAPHORE_TYPE_BINARY);
|
||||
vulkan_interface->set_signal_semaphore(vulkan_interface->handle,
|
||||
signal_semaphore->get_semaphore());
|
||||
signal_semaphore->signal_external();
|
||||
|
||||
acquire_semaphore = wsi.consume_external_release_semaphore();
|
||||
if (acquire_semaphore && acquire_semaphore->get_semaphore() != VK_NULL_HANDLE)
|
||||
{
|
||||
vulkan_interface->set_image(vulkan_interface->handle,
|
||||
&swapchain_image_info,
|
||||
1, &acquire_semaphore->get_semaphore(),
|
||||
VK_QUEUE_FAMILY_IGNORED);
|
||||
|
||||
// Lets us recycle the semaphore.
|
||||
acquire_semaphore->wait_external();
|
||||
|
||||
video_cb(RETRO_HW_FRAME_BUFFER_VALID, swapchain_width, swapchain_height, 0);
|
||||
can_dupe = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
vulkan_interface->set_image(vulkan_interface->handle,
|
||||
&swapchain_image_info,
|
||||
0, nullptr,
|
||||
VK_QUEUE_FAMILY_IGNORED);
|
||||
|
||||
if (!can_dupe)
|
||||
{
|
||||
// Need something to show ... Just clear the image to black and present that.
|
||||
// This should only happen if we don't render to swapchain the very first frame,
|
||||
// so performance doesn't really matter.
|
||||
auto &device = wsi.get_device();
|
||||
auto cmd = device.request_command_buffer();
|
||||
cmd->image_barrier(*swapchain_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, VK_PIPELINE_STAGE_2_CLEAR_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
cmd->clear_image(*swapchain_image, {});
|
||||
cmd->image_barrier(*swapchain_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
|
||||
device.submit(cmd);
|
||||
video_cb(RETRO_HW_FRAME_BUFFER_VALID, swapchain_width, swapchain_height, 0);
|
||||
can_dupe = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
video_cb(nullptr, swapchain_width, swapchain_height, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark video_cb has having done work in our frame context.
|
||||
wsi.get_device().submit_external(Vulkan::CommandBuffer::Type::Generic);
|
||||
|
||||
acquire_semaphore = signal_semaphore;
|
||||
}
|
||||
|
||||
bool libretro_context_reset(retro_hw_render_interface_vulkan *vulkan, Granite::Application &app)
|
||||
{
|
||||
vulkan_interface = vulkan;
|
||||
if (vulkan->interface_type != RETRO_HW_RENDER_INTERFACE_VULKAN)
|
||||
return false;
|
||||
|
||||
if (vulkan->interface_version != RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION)
|
||||
return false;
|
||||
|
||||
if (!app.init_wsi(std::move(vulkan_context)))
|
||||
return false;
|
||||
|
||||
auto &device = app.get_wsi().get_device();
|
||||
device.set_queue_lock([vulkan]() {
|
||||
vulkan->lock_queue(vulkan->handle);
|
||||
},
|
||||
[vulkan]() {
|
||||
vulkan->unlock_queue(vulkan->handle);
|
||||
});
|
||||
|
||||
const unsigned num_swapchain_images = 2;
|
||||
|
||||
Vulkan::ImageCreateInfo info = Vulkan::ImageCreateInfo::render_target(swapchain_width, swapchain_height,
|
||||
VK_FORMAT_R8G8B8A8_SRGB);
|
||||
info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
|
||||
swapchain_image = device.create_image(info, nullptr);
|
||||
swapchain_image->set_swapchain_layout(VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL);
|
||||
can_dupe = false;
|
||||
|
||||
Vulkan::ImageViewCreateInfo view_info;
|
||||
view_info.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
view_info.image = swapchain_image.get();
|
||||
swapchain_unorm_view = device.create_image_view(view_info);
|
||||
|
||||
std::vector<Vulkan::ImageHandle> images;
|
||||
for (unsigned i = 0; i < num_swapchain_images; i++)
|
||||
images.push_back(swapchain_image);
|
||||
|
||||
device.init_frame_contexts(2);
|
||||
if (!app.get_wsi().init_external_swapchain(std::move(images)))
|
||||
return false;
|
||||
|
||||
// Setup the swapchain image info for the frontend.
|
||||
swapchain_image_info.image_view = swapchain_unorm_view->get_view().view;
|
||||
swapchain_image_info.image_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
|
||||
swapchain_image_info.create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||
swapchain_image_info.create_info.image = swapchain_unorm_view->get_image().get_image();
|
||||
swapchain_image_info.create_info.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
swapchain_image_info.create_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
|
||||
swapchain_image_info.create_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
|
||||
swapchain_image_info.create_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
|
||||
swapchain_image_info.create_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
|
||||
swapchain_image_info.create_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
swapchain_image_info.create_info.subresourceRange.levelCount = 1;
|
||||
swapchain_image_info.create_info.subresourceRange.layerCount = 1;
|
||||
swapchain_image_info.create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
swapchain_frame_index = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void libretro_context_destroy(Granite::Application *app)
|
||||
{
|
||||
swapchain_unorm_view.reset();
|
||||
swapchain_image.reset();
|
||||
acquire_semaphore.reset();
|
||||
|
||||
if (app)
|
||||
app->teardown_wsi();
|
||||
}
|
||||
|
||||
static const VkApplicationInfo *get_application_info(void)
|
||||
{
|
||||
return &vulkan_app;
|
||||
}
|
||||
|
||||
bool libretro_load_game(retro_environment_t environ_cb)
|
||||
{
|
||||
vulkan_negotiation.interface_type = RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN;
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT, &vulkan_negotiation))
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_WARN, "GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT failed, assuming v1 only.\n");
|
||||
vulkan_negotiation.interface_version = 1;
|
||||
}
|
||||
else if (vulkan_negotiation.interface_version == 0)
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_ERROR, "Vulkan is not supported, this core cannot run.\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_INFO, "GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT passed, exposing v2.\n");
|
||||
vulkan_negotiation.interface_version = 2;
|
||||
}
|
||||
|
||||
vulkan_negotiation.create_device = Granite::libretro_create_device;
|
||||
vulkan_negotiation.create_device2 = Granite::libretro_create_device2;
|
||||
vulkan_negotiation.create_instance = Granite::libretro_create_instance;
|
||||
vulkan_negotiation.destroy_device = nullptr;
|
||||
vulkan_negotiation.get_application_info = get_application_info;
|
||||
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE, &vulkan_negotiation))
|
||||
{
|
||||
Granite::libretro_log(RETRO_LOG_ERROR, "SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE failed, this core cannot run.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *em = GRANITE_EVENT_MANAGER();
|
||||
if (em)
|
||||
{
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void libretro_unload_game()
|
||||
{
|
||||
auto *em = GRANITE_EVENT_MANAGER();
|
||||
if (em)
|
||||
{
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+71
@@ -0,0 +1,71 @@
|
||||
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "vulkan_headers.hpp"
|
||||
#include "libretro.h"
|
||||
#include "libretro_vulkan.h"
|
||||
#include "application.hpp"
|
||||
|
||||
// Various utilities to make writing a libretro Vulkan interface easier.
|
||||
// The heavy lifting of WSI interfacing with the libretro frontend is implemented here.
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
extern retro_log_printf_t libretro_log;
|
||||
|
||||
bool libretro_create_device(
|
||||
struct retro_vulkan_context *context,
|
||||
VkInstance instance,
|
||||
VkPhysicalDevice gpu,
|
||||
VkSurfaceKHR surface,
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
const char **required_device_extensions,
|
||||
unsigned num_required_device_extensions,
|
||||
const char **required_device_layers,
|
||||
unsigned num_required_device_layers,
|
||||
const VkPhysicalDeviceFeatures *required_features);
|
||||
|
||||
// Takes effect next time the swapchain is recreated, on context_reset.
|
||||
void libretro_set_swapchain_size(unsigned width, unsigned height);
|
||||
|
||||
// Used in get_application_info.
|
||||
void libretro_set_application_info(const char *name, unsigned version);
|
||||
|
||||
// Called on context_reset HW_RENDER callback.
|
||||
bool libretro_context_reset(retro_hw_render_interface_vulkan *vulkan, Granite::Application &app);
|
||||
|
||||
// Called on context_destroy HW_RENDER callback.
|
||||
void libretro_context_destroy(Granite::Application *app);
|
||||
|
||||
// Called at the start of the frame.
|
||||
void libretro_begin_frame(Vulkan::WSI &wsi, retro_usec_t frame_time);
|
||||
|
||||
// Called at the end of the frame.
|
||||
void libretro_end_frame(retro_video_refresh_t video_cb, Vulkan::WSI &wsi);
|
||||
|
||||
// Called on retro_load_game.
|
||||
bool libretro_load_game(retro_environment_t environ_cb);
|
||||
// Called on retro_unload_game.
|
||||
void libretro_unload_game();
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "../application_glue.hpp"
|
||||
#include <stddef.h>
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
int application_main(
|
||||
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
|
||||
Application *(*create_application)(int, char **), int argc, char *argv[])
|
||||
{
|
||||
(void)query_application_interface;
|
||||
(void)create_application;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+856
@@ -0,0 +1,856 @@
|
||||
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
#include <atomic>
|
||||
|
||||
#include "application.hpp"
|
||||
#include "application_wsi.hpp"
|
||||
#include "application_events.hpp"
|
||||
#include "input.hpp"
|
||||
#include "input_sdl.hpp"
|
||||
#include "cli_parser.hpp"
|
||||
#include "global_managers_init.hpp"
|
||||
#include "timeline_trace_file.hpp"
|
||||
#include "path_utils.hpp"
|
||||
#include "thread_group.hpp"
|
||||
#include "thread_id.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
static Key sdl_key_to_granite_key(SDL_Keycode key)
|
||||
{
|
||||
if (key >= 'a' && key <= 'z')
|
||||
return Key(int(Granite::Key::A) + (key - 'a'));
|
||||
else if (key >= 'A' && key <= 'Z')
|
||||
return Key(int(Granite::Key::A) + (key - 'A'));
|
||||
|
||||
#define k(sdl, granite) case SDLK_##sdl: return Key::granite
|
||||
switch (key)
|
||||
{
|
||||
k(LCTRL, LeftCtrl);
|
||||
k(LALT, LeftAlt);
|
||||
k(LSHIFT, LeftShift);
|
||||
k(RETURN, Return);
|
||||
k(SPACE, Space);
|
||||
k(ESCAPE, Escape);
|
||||
k(LEFT, Left);
|
||||
k(RIGHT, Right);
|
||||
k(UP, Up);
|
||||
k(DOWN, Down);
|
||||
k(0, _0);
|
||||
k(1, _1);
|
||||
k(2, _2);
|
||||
k(3, _3);
|
||||
k(4, _4);
|
||||
k(5, _5);
|
||||
k(6, _6);
|
||||
k(7, _7);
|
||||
k(8, _8);
|
||||
k(9, _9);
|
||||
default:
|
||||
return Key::Unknown;
|
||||
}
|
||||
#undef k
|
||||
}
|
||||
|
||||
struct WSIPlatformSDL : GraniteWSIPlatform
|
||||
{
|
||||
public:
|
||||
struct Options
|
||||
{
|
||||
unsigned override_width = 0;
|
||||
unsigned override_height = 0;
|
||||
bool fullscreen = false;
|
||||
#ifdef _WIN32
|
||||
bool threaded = true;
|
||||
#else
|
||||
bool threaded = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
explicit WSIPlatformSDL(const Options &options_)
|
||||
: options(options_)
|
||||
{
|
||||
}
|
||||
|
||||
void run_gamepad_init()
|
||||
{
|
||||
Util::Timer tmp_timer;
|
||||
tmp_timer.start();
|
||||
|
||||
if (!SDL_Init(SDL_INIT_GAMEPAD))
|
||||
{
|
||||
LOGE("Failed to init gamepad.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGI("SDL_Init(GAMEPAD) took %.3f seconds async.\n", tmp_timer.end());
|
||||
|
||||
LOGI("Pushing task to main thread.\n");
|
||||
push_task_to_main_thread([this]() {
|
||||
LOGI("Running task in main thread.\n");
|
||||
if (!pad.init(get_input_tracker(), [](std::function<void ()> func) { func(); }))
|
||||
LOGE("Failed to init gamepad tracker.\n");
|
||||
|
||||
gamepad_init_async.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
void kick_gamepad_init()
|
||||
{
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
// Adding gamepad events will make main loop spin without waiting.
|
||||
SDL_SetHint(SDL_HINT_AUTO_UPDATE_JOYSTICKS, "0");
|
||||
|
||||
// Enumerating gamepads can be extremely slow in some cases. Do this async.
|
||||
// Gamepad interface is very async friendly.
|
||||
|
||||
gamepad_init_async = false;
|
||||
|
||||
if (auto *tg = GRANITE_THREAD_GROUP())
|
||||
{
|
||||
gamepad_init_task = tg->create_task([this]() { run_gamepad_init(); });
|
||||
gamepad_init_task->set_desc("SDL init gamepad");
|
||||
gamepad_init_task->set_task_class(TaskClass::Background);
|
||||
gamepad_init_task->flush();
|
||||
}
|
||||
else
|
||||
run_gamepad_init();
|
||||
}
|
||||
|
||||
bool init(const std::string &name, unsigned width_, unsigned height_)
|
||||
{
|
||||
request_tear_down.store(false);
|
||||
width = width_;
|
||||
height = height_;
|
||||
|
||||
if (options.override_width)
|
||||
width = options.override_width;
|
||||
if (options.override_height)
|
||||
height = options.override_height;
|
||||
|
||||
#ifdef __linux__
|
||||
// RenderDoc doesn't support Wayland, and SDL3 uses Wayland by default.
|
||||
// Opt in to X11 to avoid having to manually remember to pass down SDL_VIDEO_DRIVER=x11.
|
||||
void *renderdoc_module = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
|
||||
if (renderdoc_module)
|
||||
{
|
||||
LOGI("RenderDoc is loaded, disabling Wayland.\n");
|
||||
setenv("SDL_VIDEO_DRIVER", "x11", 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
Util::Timer tmp_timer;
|
||||
tmp_timer.start();
|
||||
if (!SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO))
|
||||
{
|
||||
LOGE("Failed to init SDL.\n");
|
||||
return false;
|
||||
}
|
||||
LOGI("SDL_Init took %.3f seconds.\n", tmp_timer.end());
|
||||
|
||||
kick_gamepad_init();
|
||||
|
||||
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, false);
|
||||
SDL_SetEventEnabled(SDL_EVENT_DROP_TEXT, false);
|
||||
|
||||
if (!SDL_Vulkan_LoadLibrary(nullptr))
|
||||
{
|
||||
LOGE("Failed to load Vulkan library.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Vulkan::Context::init_loader(
|
||||
reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr())))
|
||||
{
|
||||
LOGE("Failed to initialize Vulkan loader.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
wake_event_type = SDL_RegisterEvents(1);
|
||||
|
||||
application.name = name;
|
||||
if (application.name.empty())
|
||||
application.name = Path::basename(Path::get_executable_path());
|
||||
|
||||
window = SDL_CreateWindow(application.name.empty() ? "SDL Window" : application.name.c_str(),
|
||||
int(width), int(height), SDL_WINDOW_RESIZABLE | SDL_WINDOW_VULKAN);
|
||||
if (!window)
|
||||
{
|
||||
LOGE("Failed to create SDL window.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.fullscreen)
|
||||
toggle_fullscreen();
|
||||
|
||||
application.info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
|
||||
application.info.pEngineName = "Granite";
|
||||
application.info.pApplicationName = application.name.empty() ? "Granite" : application.name.c_str();
|
||||
application.info.apiVersion = VK_API_VERSION_1_1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const VkApplicationInfo *get_application_info() override
|
||||
{
|
||||
return &application.info;
|
||||
}
|
||||
|
||||
void begin_drop_event() override
|
||||
{
|
||||
push_task_to_main_thread([]() {
|
||||
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, true);
|
||||
});
|
||||
}
|
||||
|
||||
void show_message_box(const std::string &str, MessageType type) override
|
||||
{
|
||||
push_task_to_main_thread([this, str, type]() {
|
||||
const char *title = nullptr;
|
||||
Uint32 flags = 0;
|
||||
switch (type)
|
||||
{
|
||||
case MessageType::Error:
|
||||
flags = SDL_MESSAGEBOX_ERROR;
|
||||
title = "Error";
|
||||
break;
|
||||
|
||||
case MessageType::Warning:
|
||||
flags = SDL_MESSAGEBOX_WARNING;
|
||||
title = "Warning";
|
||||
break;
|
||||
|
||||
case MessageType::Info:
|
||||
flags = SDL_MESSAGEBOX_INFORMATION;
|
||||
title = "Info";
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_ShowSimpleMessageBox(flags, title, str.c_str(), window);
|
||||
});
|
||||
}
|
||||
|
||||
uintptr_t get_native_window() override
|
||||
{
|
||||
#ifdef _WIN32
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
SDL_LockProperties(props);
|
||||
auto hwnd = static_cast<HWND>(SDL_GetPointerProperty(props, "SDL.window.win32.hwnd", nullptr));
|
||||
SDL_UnlockProperties(props);
|
||||
return reinterpret_cast<uintptr_t>(hwnd);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void toggle_fullscreen()
|
||||
{
|
||||
bool is_fullscreen = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||
|
||||
if (!is_fullscreen)
|
||||
{
|
||||
if (!SDL_SetWindowFullscreen(window, true))
|
||||
{
|
||||
LOGE("Failed to toggle fullscreen.\n");
|
||||
}
|
||||
#ifdef _WIN32
|
||||
else
|
||||
{
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
SDL_LockProperties(props);
|
||||
auto hwnd = static_cast<HWND>(SDL_GetPointerProperty(props, "SDL.window.win32.hwnd", nullptr));
|
||||
SDL_UnlockProperties(props);
|
||||
|
||||
push_task_to_async_thread([this, hwnd]() {
|
||||
set_hmonitor(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY));
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef _WIN32
|
||||
push_task_to_async_thread([this]() {
|
||||
set_hmonitor(nullptr);
|
||||
});
|
||||
#endif
|
||||
SDL_SetWindowFullscreen(window, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool alive(Vulkan::WSI &) override
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||
flush_deferred_input_events();
|
||||
process_events_async_thread();
|
||||
process_events_async_thread_non_pollable();
|
||||
return !request_tear_down.load();
|
||||
}
|
||||
|
||||
void poll_input() override
|
||||
{
|
||||
if (!options.threaded && !iterate_message_loop())
|
||||
request_tear_down = true;
|
||||
|
||||
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||
flush_deferred_input_events();
|
||||
process_events_async_thread();
|
||||
|
||||
if (gamepad_init_async.load(std::memory_order_acquire))
|
||||
pad.update(get_input_tracker());
|
||||
get_input_tracker().dispatch_current_state(get_frame_timer().get_frame_time());
|
||||
}
|
||||
|
||||
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||
begin_async_input_handling();
|
||||
{
|
||||
process_events_async_thread();
|
||||
if (gamepad_init_async.load(std::memory_order_acquire))
|
||||
pad.update(get_input_tracker());
|
||||
}
|
||||
end_async_input_handling();
|
||||
get_input_tracker().dispatch_current_state(0.0, override_handler);
|
||||
}
|
||||
|
||||
std::vector<const char *> get_instance_extensions() override
|
||||
{
|
||||
uint32_t count;
|
||||
const char * const *ext = SDL_Vulkan_GetInstanceExtensions(&count);
|
||||
return { ext, ext + count };
|
||||
}
|
||||
|
||||
VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice) override
|
||||
{
|
||||
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||
if (!SDL_Vulkan_CreateSurface(window, instance, nullptr, &surface))
|
||||
return VK_NULL_HANDLE;
|
||||
|
||||
int actual_width, actual_height;
|
||||
SDL_GetWindowSizeInPixels(window, &actual_width, &actual_height);
|
||||
width = unsigned(actual_width);
|
||||
height = unsigned(actual_height);
|
||||
return surface;
|
||||
}
|
||||
|
||||
uint32_t get_surface_width() override
|
||||
{
|
||||
return width;
|
||||
}
|
||||
|
||||
uint32_t get_surface_height() override
|
||||
{
|
||||
return height;
|
||||
}
|
||||
|
||||
~WSIPlatformSDL()
|
||||
{
|
||||
if (gamepad_init_task)
|
||||
gamepad_init_task->wait();
|
||||
|
||||
if (window)
|
||||
SDL_DestroyWindow(window);
|
||||
|
||||
pad.close();
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
void block_until_wsi_forward_progress(Vulkan::WSI &wsi) override
|
||||
{
|
||||
if (options.threaded)
|
||||
{
|
||||
get_frame_timer().enter_idle();
|
||||
while (!resize && alive(wsi))
|
||||
process_events_async_thread_blocking();
|
||||
get_frame_timer().leave_idle();
|
||||
}
|
||||
else
|
||||
{
|
||||
WSIPlatform::block_until_wsi_forward_progress(wsi);
|
||||
}
|
||||
}
|
||||
|
||||
void notify_resize(unsigned width_, unsigned height_)
|
||||
{
|
||||
LOGI("Resize: %u x %u\n", width_, height_);
|
||||
push_task_to_async_thread([=]() {
|
||||
resize = true;
|
||||
width = width_;
|
||||
height = height_;
|
||||
});
|
||||
}
|
||||
|
||||
void notify_current_swapchain_dimensions(unsigned width_, unsigned height_) override
|
||||
{
|
||||
push_task_to_main_thread([=]() {
|
||||
WSIPlatform::notify_current_swapchain_dimensions(width_, height_);
|
||||
});
|
||||
}
|
||||
|
||||
void set_window_title(const std::string &title) override
|
||||
{
|
||||
push_task_to_main_thread([=]() {
|
||||
if (window)
|
||||
SDL_SetWindowTitle(window, title.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
bool process_sdl_event(const SDL_Event &e)
|
||||
{
|
||||
if (e.type == wake_event_type)
|
||||
{
|
||||
LOGI("Processing events main thread.\n");
|
||||
process_events_main_thread();
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto dispatcher = [this](std::function<void ()> func) {
|
||||
push_task_to_async_thread(std::move(func));
|
||||
};
|
||||
|
||||
if (pad.process_sdl_event(e, get_input_tracker(), dispatcher))
|
||||
return true;
|
||||
|
||||
switch (e.type)
|
||||
{
|
||||
case SDL_EVENT_QUIT:
|
||||
return false;
|
||||
|
||||
case SDL_EVENT_WINDOW_RESIZED:
|
||||
if (e.window.windowID == SDL_GetWindowID(window))
|
||||
notify_resize(e.window.data1, e.window.data2);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
if (e.button.windowID == SDL_GetWindowID(window))
|
||||
{
|
||||
MouseButton btn;
|
||||
if (e.button.button == SDL_BUTTON_LEFT)
|
||||
btn = MouseButton::Left;
|
||||
else if (e.button.button == SDL_BUTTON_MIDDLE)
|
||||
btn = MouseButton::Middle;
|
||||
else if (e.button.button == SDL_BUTTON_RIGHT)
|
||||
btn = MouseButton::Right;
|
||||
else
|
||||
break;
|
||||
|
||||
push_task_to_async_thread(
|
||||
[this, btn, x = e.button.x, y = e.button.y,
|
||||
pressed = e.type == SDL_EVENT_MOUSE_BUTTON_DOWN]() {
|
||||
get_input_tracker().mouse_button_event(btn, x, y, pressed);
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_MOUSE_ENTER:
|
||||
if (e.window.windowID == SDL_GetWindowID(window))
|
||||
{
|
||||
float x, y;
|
||||
SDL_GetMouseState(&x, &y);
|
||||
push_task_to_async_thread([this, x, y]() {
|
||||
get_input_tracker().mouse_enter(x, y);
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
if (e.window.windowID == SDL_GetWindowID(window))
|
||||
{
|
||||
push_task_to_async_thread([this]() {
|
||||
get_input_tracker().mouse_leave();
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
if (e.motion.windowID == SDL_GetWindowID(window))
|
||||
{
|
||||
push_task_to_async_thread([this, x = e.motion.x, y = e.motion.y]() {
|
||||
get_input_tracker().mouse_move_event_absolute(x, y);
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP:
|
||||
if (e.key.windowID == SDL_GetWindowID(window))
|
||||
{
|
||||
KeyState state;
|
||||
if (e.key.repeat)
|
||||
state = KeyState::Repeat;
|
||||
else if (e.type == SDL_EVENT_KEY_DOWN)
|
||||
state = KeyState::Pressed;
|
||||
else
|
||||
state = KeyState::Released;
|
||||
|
||||
if (state == KeyState::Pressed && e.key.key == SDLK_ESCAPE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (state == KeyState::Pressed && e.key.key == SDLK_RETURN &&
|
||||
(e.key.mod & SDL_KMOD_ALT) != 0)
|
||||
{
|
||||
toggle_fullscreen();
|
||||
}
|
||||
else if (state == KeyState::Pressed && tolower(e.key.key) == 'v' &&
|
||||
(e.key.mod & SDL_KMOD_LCTRL) != 0)
|
||||
{
|
||||
push_non_pollable_task_to_async_thread([c = clipboard]() mutable {
|
||||
if (auto *manager = GRANITE_EVENT_MANAGER())
|
||||
manager->enqueue<Vulkan::ApplicationWindowTextDropEvent>(std::move(c));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Key key = sdl_key_to_granite_key(e.key.key);
|
||||
push_task_to_async_thread([=]() {
|
||||
get_input_tracker().key_event(key, state);
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_DROP_FILE:
|
||||
if (e.drop.windowID == SDL_GetWindowID(window))
|
||||
{
|
||||
std::string str = e.drop.data;
|
||||
push_non_pollable_task_to_async_thread([s = std::move(str)]() mutable {
|
||||
if (auto *manager = GRANITE_EVENT_MANAGER())
|
||||
manager->enqueue<Vulkan::ApplicationWindowFileDropEvent>(std::move(s));
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_DROP_COMPLETE:
|
||||
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, false);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_CLIPBOARD_UPDATE:
|
||||
if (SDL_HasClipboardText())
|
||||
{
|
||||
const char *text = SDL_GetClipboardText();
|
||||
if (text)
|
||||
clipboard = text;
|
||||
else
|
||||
clipboard.clear();
|
||||
}
|
||||
else
|
||||
clipboard.clear();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void run_message_loop()
|
||||
{
|
||||
SDL_Event e;
|
||||
while (async_loop_alive && SDL_WaitEvent(&e))
|
||||
if (!process_sdl_event(e))
|
||||
break;
|
||||
}
|
||||
|
||||
bool iterate_message_loop()
|
||||
{
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e))
|
||||
if (!process_sdl_event(e))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void run_loop(Application *app)
|
||||
{
|
||||
auto ctx = Global::create_thread_context();
|
||||
|
||||
process_events_main_thread();
|
||||
|
||||
if (options.threaded)
|
||||
{
|
||||
async_loop_alive = true;
|
||||
threaded_main_loop = std::thread(&WSIPlatformSDL::thread_main, this, app, std::move(ctx));
|
||||
|
||||
run_message_loop();
|
||||
notify_close();
|
||||
|
||||
if (threaded_main_loop.joinable())
|
||||
threaded_main_loop.join();
|
||||
}
|
||||
else
|
||||
thread_main(app, {});
|
||||
}
|
||||
|
||||
static void dispatch_running_events()
|
||||
{
|
||||
auto *em = GRANITE_EVENT_MANAGER();
|
||||
if (em)
|
||||
{
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
|
||||
}
|
||||
}
|
||||
|
||||
static void dispatch_stopped_events()
|
||||
{
|
||||
auto *em = GRANITE_EVENT_MANAGER();
|
||||
if (em)
|
||||
{
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||
}
|
||||
}
|
||||
|
||||
void thread_main(Application *app, Global::GlobalManagersHandle ctx)
|
||||
{
|
||||
if (options.threaded)
|
||||
{
|
||||
// Set this up as an alternative main thread.
|
||||
ThreadGroup::set_async_main_thread();
|
||||
Global::set_thread_context(*ctx);
|
||||
Util::register_thread_index(0);
|
||||
ctx.reset();
|
||||
}
|
||||
|
||||
{
|
||||
GRANITE_SCOPED_TIMELINE_EVENT("sdl-dispatch-running-events");
|
||||
dispatch_running_events();
|
||||
}
|
||||
|
||||
{
|
||||
{
|
||||
GRANITE_SCOPED_TIMELINE_EVENT("sdl-start-audio-system");
|
||||
Granite::Global::start_audio_system();
|
||||
}
|
||||
|
||||
while (app->poll())
|
||||
app->run_frame();
|
||||
Granite::Global::stop_audio_system();
|
||||
}
|
||||
dispatch_stopped_events();
|
||||
push_task_to_main_thread([this]() { async_loop_alive = false; });
|
||||
}
|
||||
|
||||
void notify_close()
|
||||
{
|
||||
request_tear_down.store(true);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
void set_hmonitor(HMONITOR monitor)
|
||||
{
|
||||
current_hmonitor = monitor;
|
||||
}
|
||||
|
||||
uintptr_t get_fullscreen_monitor() override
|
||||
{
|
||||
return reinterpret_cast<uintptr_t>(current_hmonitor);
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename Op>
|
||||
void push_task_to_async_thread(Op &&op)
|
||||
{
|
||||
push_task_to_list(task_list_async, std::forward<Op>(op));
|
||||
}
|
||||
|
||||
template <typename Op>
|
||||
void push_non_pollable_task_to_async_thread(Op &&op)
|
||||
{
|
||||
push_non_pollable_task_to_list(task_list_async, std::forward<Op>(op));
|
||||
}
|
||||
|
||||
private:
|
||||
SDL_Window *window = nullptr;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
uint32_t wake_event_type = 0;
|
||||
Options options;
|
||||
std::string clipboard;
|
||||
TaskGroupHandle gamepad_init_task;
|
||||
std::atomic<bool> gamepad_init_async;
|
||||
|
||||
struct
|
||||
{
|
||||
VkApplicationInfo info = {};
|
||||
std::string name;
|
||||
} application;
|
||||
|
||||
std::thread threaded_main_loop;
|
||||
struct TaskList
|
||||
{
|
||||
std::mutex lock;
|
||||
std::condition_variable cond;
|
||||
std::vector<std::function<void ()>> list;
|
||||
std::vector<std::function<void ()>> non_pollable_list;
|
||||
} task_list_main, task_list_async;
|
||||
|
||||
static void process_events_for_list(TaskList &list, bool blocking)
|
||||
{
|
||||
std::unique_lock<std::mutex> holder{list.lock};
|
||||
|
||||
if (blocking)
|
||||
while (list.list.empty())
|
||||
list.cond.wait(holder, [&list]() { return !list.list.empty(); });
|
||||
|
||||
for (auto &task : list.list)
|
||||
task();
|
||||
list.list.clear();
|
||||
}
|
||||
|
||||
template <typename Op>
|
||||
void push_task_to_list(TaskList &list, Op &&op)
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{list.lock};
|
||||
list.list.emplace_back(std::forward<Op>(op));
|
||||
list.cond.notify_one();
|
||||
}
|
||||
|
||||
template <typename Op>
|
||||
void push_non_pollable_task_to_list(TaskList &list, Op &&op)
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{list.lock};
|
||||
list.non_pollable_list.emplace_back(std::forward<Op>(op));
|
||||
list.cond.notify_one();
|
||||
}
|
||||
|
||||
void process_events_main_thread()
|
||||
{
|
||||
process_events_for_list(task_list_main, false);
|
||||
}
|
||||
|
||||
void process_events_main_thread_blocking()
|
||||
{
|
||||
process_events_for_list(task_list_main, true);
|
||||
}
|
||||
|
||||
void process_events_async_thread()
|
||||
{
|
||||
process_events_for_list(task_list_async, false);
|
||||
}
|
||||
|
||||
void process_events_async_thread_non_pollable()
|
||||
{
|
||||
std::unique_lock<std::mutex> holder{task_list_async.lock};
|
||||
for (auto &task : task_list_async.non_pollable_list)
|
||||
task();
|
||||
task_list_async.non_pollable_list.clear();
|
||||
}
|
||||
|
||||
void process_events_async_thread_blocking()
|
||||
{
|
||||
process_events_for_list(task_list_async, true);
|
||||
}
|
||||
|
||||
InputTrackerSDL pad;
|
||||
|
||||
template <typename Op>
|
||||
void push_task_to_main_thread(Op &&op)
|
||||
{
|
||||
push_task_to_list(task_list_main, std::forward<Op>(op));
|
||||
SDL_Event wake_event = {};
|
||||
wake_event.type = wake_event_type;
|
||||
SDL_PushEvent(&wake_event);
|
||||
}
|
||||
|
||||
std::atomic_bool request_tear_down;
|
||||
bool async_loop_alive = false;
|
||||
|
||||
#ifdef _WIN32
|
||||
HMONITOR current_hmonitor = nullptr;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
namespace Granite
|
||||
{
|
||||
int application_main(
|
||||
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
|
||||
Application *(*create_application)(int, char **), int argc, char *argv[])
|
||||
{
|
||||
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
|
||||
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
|
||||
Global::init(flags.manager_feature_flags);
|
||||
|
||||
WSIPlatformSDL::Options options;
|
||||
int exit_code;
|
||||
|
||||
Util::CLICallbacks cbs;
|
||||
cbs.add("--fullscreen", [&](Util::CLIParser &) { options.fullscreen = true; });
|
||||
cbs.add("--width", [&](Util::CLIParser &parser) { options.override_width = parser.next_uint(); });
|
||||
cbs.add("--height", [&](Util::CLIParser &parser) { options.override_height = parser.next_uint(); });
|
||||
cbs.add("--thread-main-loop", [&](Util::CLIParser &) { options.threaded = true; });
|
||||
cbs.add("--no-thread-main-loop", [&](Util::CLIParser &) { options.threaded = false; });
|
||||
cbs.error_handler = [&]() { LOGE("Failed to parse CLI arguments for SDL.\n"); };
|
||||
if (!Util::parse_cli_filtered(std::move(cbs), argc, argv, exit_code))
|
||||
return exit_code;
|
||||
|
||||
auto app = std::unique_ptr<Application>(create_application(argc, argv));
|
||||
int ret;
|
||||
|
||||
if (app)
|
||||
{
|
||||
auto platform = std::make_unique<WSIPlatformSDL>(options);
|
||||
auto *platform_handle = platform.get();
|
||||
|
||||
if (!platform->init(app->get_name(), app->get_default_width(), app->get_default_height()))
|
||||
return 1;
|
||||
|
||||
if (!app->init_platform(std::move(platform)) || !app->init_wsi())
|
||||
return 1;
|
||||
|
||||
platform_handle->run_loop(app.get());
|
||||
|
||||
app.reset();
|
||||
ret = EXIT_SUCCESS;
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = EXIT_FAILURE;
|
||||
}
|
||||
|
||||
Global::deinit();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
+3964
File diff suppressed because it is too large
Load Diff
Vendored
+494
@@ -0,0 +1,494 @@
|
||||
/* Copyright (C) 2010-2020 The RetroArch team
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------------
|
||||
* The following license statement only applies to this libretro API header (libretro_vulkan.h)
|
||||
* ---------------------------------------------------------------------------------------------
|
||||
*
|
||||
* Permission is hereby granted, free of charge,
|
||||
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LIBRETRO_VULKAN_H__
|
||||
#define LIBRETRO_VULKAN_H__
|
||||
|
||||
#include <libretro.h>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#define RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION 5
|
||||
#define RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION 2
|
||||
|
||||
struct retro_vulkan_image
|
||||
{
|
||||
VkImageView image_view;
|
||||
VkImageLayout image_layout;
|
||||
VkImageViewCreateInfo create_info;
|
||||
};
|
||||
|
||||
typedef void (*retro_vulkan_set_image_t)(void *handle,
|
||||
const struct retro_vulkan_image *image,
|
||||
uint32_t num_semaphores,
|
||||
const VkSemaphore *semaphores,
|
||||
uint32_t src_queue_family);
|
||||
|
||||
typedef uint32_t (*retro_vulkan_get_sync_index_t)(void *handle);
|
||||
typedef uint32_t (*retro_vulkan_get_sync_index_mask_t)(void *handle);
|
||||
typedef void (*retro_vulkan_set_command_buffers_t)(void *handle,
|
||||
uint32_t num_cmd,
|
||||
const VkCommandBuffer *cmd);
|
||||
typedef void (*retro_vulkan_wait_sync_index_t)(void *handle);
|
||||
typedef void (*retro_vulkan_lock_queue_t)(void *handle);
|
||||
typedef void (*retro_vulkan_unlock_queue_t)(void *handle);
|
||||
typedef void (*retro_vulkan_set_signal_semaphore_t)(void *handle, VkSemaphore semaphore);
|
||||
|
||||
typedef const VkApplicationInfo *(*retro_vulkan_get_application_info_t)(void);
|
||||
|
||||
struct retro_vulkan_context
|
||||
{
|
||||
VkPhysicalDevice gpu;
|
||||
VkDevice device;
|
||||
VkQueue queue;
|
||||
uint32_t queue_family_index;
|
||||
VkQueue presentation_queue;
|
||||
uint32_t presentation_queue_family_index;
|
||||
};
|
||||
|
||||
/* This is only used in v1 of the negotiation interface.
|
||||
* It is deprecated since it cannot express PDF2 features or optional extensions. */
|
||||
typedef bool (*retro_vulkan_create_device_t)(
|
||||
struct retro_vulkan_context *context,
|
||||
VkInstance instance,
|
||||
VkPhysicalDevice gpu,
|
||||
VkSurfaceKHR surface,
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
const char **required_device_extensions,
|
||||
unsigned num_required_device_extensions,
|
||||
const char **required_device_layers,
|
||||
unsigned num_required_device_layers,
|
||||
const VkPhysicalDeviceFeatures *required_features);
|
||||
|
||||
typedef void (*retro_vulkan_destroy_device_t)(void);
|
||||
|
||||
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
|
||||
typedef VkInstance (*retro_vulkan_create_instance_wrapper_t)(
|
||||
void *opaque, const VkInstanceCreateInfo *create_info);
|
||||
|
||||
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
|
||||
typedef VkInstance (*retro_vulkan_create_instance_t)(
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
const VkApplicationInfo *app,
|
||||
retro_vulkan_create_instance_wrapper_t create_instance_wrapper,
|
||||
void *opaque);
|
||||
|
||||
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
|
||||
typedef VkDevice (*retro_vulkan_create_device_wrapper_t)(
|
||||
VkPhysicalDevice gpu, void *opaque,
|
||||
const VkDeviceCreateInfo *create_info);
|
||||
|
||||
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
|
||||
typedef bool (*retro_vulkan_create_device2_t)(
|
||||
struct retro_vulkan_context *context,
|
||||
VkInstance instance,
|
||||
VkPhysicalDevice gpu,
|
||||
VkSurfaceKHR surface,
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
retro_vulkan_create_device_wrapper_t create_device_wrapper,
|
||||
void *opaque);
|
||||
|
||||
/* Note on thread safety:
|
||||
* The Vulkan API is heavily designed around multi-threading, and
|
||||
* the libretro interface for it should also be threading friendly.
|
||||
* A core should be able to build command buffers and submit
|
||||
* command buffers to the GPU from any thread.
|
||||
*/
|
||||
|
||||
struct retro_hw_render_context_negotiation_interface_vulkan
|
||||
{
|
||||
/* Must be set to RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN. */
|
||||
enum retro_hw_render_context_negotiation_interface_type interface_type;
|
||||
/* Usually set to RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION,
|
||||
* but can be lower depending on GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT. */
|
||||
unsigned interface_version;
|
||||
|
||||
/* If non-NULL, returns a VkApplicationInfo struct that the frontend can use instead of
|
||||
* its "default" application info.
|
||||
* VkApplicationInfo::apiVersion also controls the target core Vulkan version for instance level functionality.
|
||||
* Lifetime of the returned pointer must remain until the retro_vulkan_context is initialized.
|
||||
*
|
||||
* NOTE: For optimal compatibility with e.g. Android which is very slow to update its loader,
|
||||
* a core version of 1.1 should be requested. Features beyond that can be requested with extensions.
|
||||
* Vulkan 1.0 is only appropriate for legacy cores, but is still supported.
|
||||
* A frontend is free to bump the instance creation apiVersion as necessary if the frontend requires more advanced core features.
|
||||
*
|
||||
* v2: This function must not be NULL, and must not return NULL.
|
||||
* v1: It was not clearly defined if this function could return NULL.
|
||||
* Frontends should be defensive and provide a default VkApplicationInfo
|
||||
* if this function returns NULL or if this function is NULL.
|
||||
*/
|
||||
retro_vulkan_get_application_info_t get_application_info;
|
||||
|
||||
/* If non-NULL, the libretro core will choose one or more physical devices,
|
||||
* create one or more logical devices and create one or more queues.
|
||||
* The core must prepare a designated PhysicalDevice, Device, Queue and queue family index
|
||||
* which the frontend will use for its internal operation.
|
||||
*
|
||||
* If gpu is not VK_NULL_HANDLE, the physical device provided to the frontend must be this PhysicalDevice if the call succeeds.
|
||||
* The core is still free to use other physical devices for other purposes that are private to the core.
|
||||
*
|
||||
* The frontend will request certain extensions and layers for a device which is created.
|
||||
* The core must ensure that the queue and queue_family_index support GRAPHICS and COMPUTE.
|
||||
*
|
||||
* If surface is not VK_NULL_HANDLE, the core must consider presentation when creating the queues.
|
||||
* If presentation to "surface" is supported on the queue, presentation_queue must be equal to queue.
|
||||
* If not, a second queue must be provided in presentation_queue and presentation_queue_index.
|
||||
* If surface is not VK_NULL_HANDLE, the instance from frontend will have been created with supported for
|
||||
* VK_KHR_surface extension.
|
||||
*
|
||||
* The core is free to set its own queue priorities.
|
||||
* Device provided to frontend is owned by the frontend, but any additional device resources must be freed by core
|
||||
* in destroy_device callback.
|
||||
*
|
||||
* If this function returns true, a PhysicalDevice, Device and Queues are initialized.
|
||||
* If false, none of the above have been initialized and the frontend will attempt
|
||||
* to fallback to "default" device creation, as if this function was never called.
|
||||
*/
|
||||
retro_vulkan_create_device_t create_device;
|
||||
|
||||
/* If non-NULL, this callback is called similar to context_destroy for HW_RENDER_INTERFACE.
|
||||
* However, it will be called even if context_reset was not called.
|
||||
* This can happen if the context never succeeds in being created.
|
||||
* destroy_device will always be called before the VkInstance
|
||||
* of the frontend is destroyed if create_device was called successfully so that the core has a chance of
|
||||
* tearing down its own device resources.
|
||||
*
|
||||
* Only auxillary resources should be freed here, i.e. resources which are not part of retro_vulkan_context.
|
||||
* v2: Auxillary instance resources created during create_instance can also be freed here.
|
||||
*/
|
||||
retro_vulkan_destroy_device_t destroy_device;
|
||||
|
||||
/* v2 API: If interface_version is < 2, fields below must be ignored.
|
||||
* If the frontend does not support interface version 2, the v1 entry points will be used instead. */
|
||||
|
||||
/* If non-NULL, this is called to create an instance, otherwise a VkInstance is created by the frontend.
|
||||
* v1 interface bug: The only way to enable instance features is through core versions signalled in VkApplicationInfo.
|
||||
* The frontend may request that certain extensions and layers
|
||||
* are enabled on the VkInstance. Application may add additional features.
|
||||
* If app is non-NULL, apiVersion controls the minimum core version required by the application.
|
||||
* Return a VkInstance or VK_NULL_HANDLE. The VkInstance is owned by the frontend.
|
||||
*
|
||||
* Rather than call vkCreateInstance directly, a core must call the CreateInstance wrapper provided with:
|
||||
* VkInstance instance = create_instance_wrapper(opaque, &create_info);
|
||||
* If the core wishes to create a private instance for whatever reason (relying on shared memory for example),
|
||||
* it may call vkCreateInstance directly. */
|
||||
retro_vulkan_create_instance_t create_instance;
|
||||
|
||||
/* If non-NULL and frontend recognizes negotiation interface >= 2, create_device2 takes precedence over create_device.
|
||||
* Similar to create_device, but is extended to better understand new core versions and PDF2 feature enablement.
|
||||
* Requirements for create_device2 are the same as create_device unless a difference is mentioned.
|
||||
*
|
||||
* v2 consideration:
|
||||
* If the chosen gpu by frontend cannot be supported, a core must return false.
|
||||
*
|
||||
* NOTE: "Cannot be supported" is intentionally vaguely defined.
|
||||
* Refusing to run on an iGPU for a very intensive core with desktop GPU as a minimum spec may be in the gray area.
|
||||
* Not supporting optional features is not a good reason to reject a physical device, however.
|
||||
*
|
||||
* On device creation feature with explicit gpu, a frontend should fall back create_device2 with gpu == VK_NULL_HANDLE and let core
|
||||
* decide on a supported device if possible.
|
||||
*
|
||||
* A core must assume that the explicitly provided GPU is the only guaranteed attempt it has to create a device.
|
||||
* A fallback may not be attempted if there are particular reasons why only a specific physical device can work,
|
||||
* but these situations should be esoteric and rare in nature, e.g. a libretro frontend is implemented with external memory
|
||||
* and only LUID matching would work.
|
||||
* Cores and frontends should ensure "best effort" when negotiating like this and appropriate logging is encouraged.
|
||||
*
|
||||
* v1 note: In the v1 version of create_device, it was never expected that create_device would fail like this,
|
||||
* and frontends are not expected to attempt fall backs.
|
||||
*
|
||||
* Rather than call vkCreateDevice directly, a core must call the CreateDevice wrapper provided with:
|
||||
* VkDevice device = create_device_wrapper(gpu, opaque, &create_info);
|
||||
* If the core wishes to create a private device for whatever reason (relying on shared memory for example),
|
||||
* it may call vkCreateDevice directly.
|
||||
*
|
||||
* This allows the frontend to add additional extensions that it requires as well as adjust the PDF2 pNext as required.
|
||||
* It is also possible adjust the queue create infos in case the frontend desires to allocate some private queues.
|
||||
*
|
||||
* The get_instance_proc_addr provided in create_device2 must be the same as create_instance.
|
||||
*
|
||||
* NOTE: The frontend must not disable features requested by application.
|
||||
* NOTE: The frontend must not add any robustness features as some API behavior may change (VK_EXT_descriptor_buffer comes to mind).
|
||||
* I.e. robustBufferAccess and the like. (nullDescriptor from robustness2 is allowed to be enabled).
|
||||
*/
|
||||
retro_vulkan_create_device2_t create_device2;
|
||||
};
|
||||
|
||||
struct retro_hw_render_interface_vulkan
|
||||
{
|
||||
/* Must be set to RETRO_HW_RENDER_INTERFACE_VULKAN. */
|
||||
enum retro_hw_render_interface_type interface_type;
|
||||
/* Must be set to RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION. */
|
||||
unsigned interface_version;
|
||||
|
||||
/* Opaque handle to the Vulkan backend in the frontend
|
||||
* which must be passed along to all function pointers
|
||||
* in this interface.
|
||||
*
|
||||
* The rationale for including a handle here (which libretro v1
|
||||
* doesn't currently do in general) is:
|
||||
*
|
||||
* - Vulkan cores should be able to be freely threaded without lots of fuzz.
|
||||
* This would break frontends which currently rely on TLS
|
||||
* to deal with multiple cores loaded at the same time.
|
||||
* - Fixing this in general is TODO for an eventual libretro v2.
|
||||
*/
|
||||
void *handle;
|
||||
|
||||
/* The Vulkan instance the context is using. */
|
||||
VkInstance instance;
|
||||
/* The physical device used. */
|
||||
VkPhysicalDevice gpu;
|
||||
/* The logical device used. */
|
||||
VkDevice device;
|
||||
|
||||
/* Allows a core to fetch all its needed symbols without having to link
|
||||
* against the loader itself. */
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr;
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr;
|
||||
|
||||
/* The queue the core must use to submit data.
|
||||
* This queue and index must remain constant throughout the lifetime
|
||||
* of the context.
|
||||
*
|
||||
* This queue will be the queue that supports graphics and compute
|
||||
* if the device supports compute.
|
||||
*/
|
||||
VkQueue queue;
|
||||
unsigned queue_index;
|
||||
|
||||
/* Before calling retro_video_refresh_t with RETRO_HW_FRAME_BUFFER_VALID,
|
||||
* set which image to use for this frame.
|
||||
*
|
||||
* If num_semaphores is non-zero, the frontend will wait for the
|
||||
* semaphores provided to be signaled before using the results further
|
||||
* in the pipeline.
|
||||
*
|
||||
* Semaphores provided by a single call to set_image will only be
|
||||
* waited for once (waiting for a semaphore resets it).
|
||||
* E.g. set_image, video_refresh, and then another
|
||||
* video_refresh without set_image,
|
||||
* but same image will only wait for semaphores once.
|
||||
*
|
||||
* For this reason, ownership transfer will only occur if semaphores
|
||||
* are waited on for a particular frame in the frontend.
|
||||
*
|
||||
* Using semaphores is optional for synchronization purposes,
|
||||
* but if not using
|
||||
* semaphores, an image memory barrier in vkCmdPipelineBarrier
|
||||
* should be used in the graphics_queue.
|
||||
* Example:
|
||||
*
|
||||
* vkCmdPipelineBarrier(cmd,
|
||||
* srcStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
|
||||
* dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
* image_memory_barrier = {
|
||||
* srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
* dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
|
||||
* });
|
||||
*
|
||||
* The use of pipeline barriers instead of semaphores is encouraged
|
||||
* as it is simpler and more fine-grained. A layout transition
|
||||
* must generally happen anyways which requires a
|
||||
* pipeline barrier.
|
||||
*
|
||||
* The image passed to set_image must have imageUsage flags set to at least
|
||||
* VK_IMAGE_USAGE_TRANSFER_SRC_BIT and VK_IMAGE_USAGE_SAMPLED_BIT.
|
||||
* The core will naturally want to use flags such as
|
||||
* VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT and/or
|
||||
* VK_IMAGE_USAGE_TRANSFER_DST_BIT depending
|
||||
* on how the final image is created.
|
||||
*
|
||||
* The image must also have been created with MUTABLE_FORMAT bit set if
|
||||
* 8-bit formats are used, so that the frontend can reinterpret sRGB
|
||||
* formats as it sees fit.
|
||||
*
|
||||
* Images passed to set_image should be created with TILING_OPTIMAL.
|
||||
* The image layout should be transitioned to either
|
||||
* VK_IMAGE_LAYOUT_GENERIC or VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
|
||||
* The actual image layout used must be set in image_layout.
|
||||
*
|
||||
* The image must be a 2D texture which may or not be layered
|
||||
* and/or mipmapped.
|
||||
*
|
||||
* The image must be suitable for linear sampling.
|
||||
* While the image_view is typically the only field used,
|
||||
* the frontend may want to reinterpret the texture as sRGB vs.
|
||||
* non-sRGB for example so the VkImageViewCreateInfo used to
|
||||
* create the image view must also be passed in.
|
||||
*
|
||||
* The data in the pointer to the image struct will not be copied
|
||||
* as the pNext field in create_info cannot be reliably deep-copied.
|
||||
* The image pointer passed to set_image must be valid until
|
||||
* retro_video_refresh_t has returned.
|
||||
*
|
||||
* If frame duping is used when passing NULL to retro_video_refresh_t,
|
||||
* the frontend is free to either use the latest image passed to
|
||||
* set_image or reuse the older pointer passed to set_image the
|
||||
* frame RETRO_HW_FRAME_BUFFER_VALID was last used.
|
||||
*
|
||||
* Essentially, the lifetime of the pointer passed to
|
||||
* retro_video_refresh_t should be extended if frame duping is used
|
||||
* so that the frontend can reuse the older pointer.
|
||||
*
|
||||
* The image itself however, must not be touched by the core until
|
||||
* wait_sync_index has been completed later. The frontend may perform
|
||||
* layout transitions on the image, so even read-only access is not defined.
|
||||
* The exception to read-only rule is if GENERAL layout is used for the image.
|
||||
* In this case, the frontend is not allowed to perform any layout transitions,
|
||||
* so concurrent reads from core and frontend are allowed.
|
||||
*
|
||||
* If frame duping is used, or if set_command_buffers is used,
|
||||
* the frontend will not wait for any semaphores.
|
||||
*
|
||||
* The src_queue_family is used to specify which queue family
|
||||
* the image is currently owned by. If using multiple queue families
|
||||
* (e.g. async compute), the frontend will need to acquire ownership of the
|
||||
* image before rendering with it and release the image afterwards.
|
||||
*
|
||||
* If src_queue_family is equal to the queue family (queue_index),
|
||||
* no ownership transfer will occur.
|
||||
* Similarly, if src_queue_family is VK_QUEUE_FAMILY_IGNORED,
|
||||
* no ownership transfer will occur.
|
||||
*
|
||||
* The frontend will always release ownership back to src_queue_family.
|
||||
* Waiting for frontend to complete with wait_sync_index() ensures that
|
||||
* the frontend has released ownership back to the application.
|
||||
* Note that in Vulkan, transfering ownership is a two-part process.
|
||||
*
|
||||
* Example frame:
|
||||
* - core releases ownership from src_queue_index to queue_index with VkImageMemoryBarrier.
|
||||
* - core calls set_image with src_queue_index.
|
||||
* - Frontend will acquire the image with src_queue_index -> queue_index as well, completing the ownership transfer.
|
||||
* - Frontend renders the frame.
|
||||
* - Frontend releases ownership with queue_index -> src_queue_index.
|
||||
* - Next time image is used, core must acquire ownership from queue_index ...
|
||||
*
|
||||
* Since the frontend releases ownership, we cannot necessarily dupe the frame because
|
||||
* the core needs to make the roundtrip of ownership transfer.
|
||||
*/
|
||||
retro_vulkan_set_image_t set_image;
|
||||
|
||||
/* Get the current sync index for this frame which is obtained in
|
||||
* frontend by calling e.g. vkAcquireNextImageKHR before calling
|
||||
* retro_run().
|
||||
*
|
||||
* This index will correspond to which swapchain buffer is currently
|
||||
* the active one.
|
||||
*
|
||||
* Knowing this index is very useful for maintaining safe asynchronous CPU
|
||||
* and GPU operation without stalling.
|
||||
*
|
||||
* The common pattern for synchronization is to receive fences when
|
||||
* submitting command buffers to Vulkan (vkQueueSubmit) and add this fence
|
||||
* to a list of fences for frame number get_sync_index().
|
||||
*
|
||||
* Next time we receive the same get_sync_index(), we can wait for the
|
||||
* fences from before, which will usually return immediately as the
|
||||
* frontend will generally also avoid letting the GPU run ahead too much.
|
||||
*
|
||||
* After the fence has signaled, we know that the GPU has completed all
|
||||
* GPU work related to work submitted in the frame we last saw get_sync_index().
|
||||
*
|
||||
* This means we can safely reuse or free resources allocated in this frame.
|
||||
*
|
||||
* In theory, even if we wait for the fences correctly, it is not technically
|
||||
* safe to write to the image we earlier passed to the frontend since we're
|
||||
* not waiting for the frontend GPU jobs to complete.
|
||||
*
|
||||
* The frontend will guarantee that the appropriate pipeline barrier
|
||||
* in graphics_queue has been used such that
|
||||
* VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT cannot
|
||||
* start until the frontend is done with the image.
|
||||
*/
|
||||
retro_vulkan_get_sync_index_t get_sync_index;
|
||||
|
||||
/* Returns a bitmask of how many swapchain images we currently have
|
||||
* in the frontend.
|
||||
*
|
||||
* If bit #N is set in the return value, get_sync_index can return N.
|
||||
* Knowing this value is useful for preallocating per-frame management
|
||||
* structures ahead of time.
|
||||
*
|
||||
* While this value will typically remain constant throughout the
|
||||
* applications lifecycle, it may for example change if the frontend
|
||||
* suddently changes fullscreen state and/or latency.
|
||||
*
|
||||
* If this value ever changes, it is safe to assume that the device
|
||||
* is completely idle and all synchronization objects can be deleted
|
||||
* right away as desired.
|
||||
*/
|
||||
retro_vulkan_get_sync_index_mask_t get_sync_index_mask;
|
||||
|
||||
/* Instead of submitting the command buffer to the queue first, the core
|
||||
* can pass along its command buffer to the frontend, and the frontend
|
||||
* will submit the command buffer together with the frontends command buffers.
|
||||
*
|
||||
* This has the advantage that the overhead of vkQueueSubmit can be
|
||||
* amortized into a single call. For this mode, semaphores in set_image
|
||||
* will be ignored, so vkCmdPipelineBarrier must be used to synchronize
|
||||
* the core and frontend.
|
||||
*
|
||||
* The command buffers in set_command_buffers are only executed once,
|
||||
* even if frame duping is used.
|
||||
*
|
||||
* If frame duping is used, set_image should be used for the frames
|
||||
* which should be duped instead.
|
||||
*
|
||||
* Command buffers passed to the frontend with set_command_buffers
|
||||
* must not actually be submitted to the GPU until retro_video_refresh_t
|
||||
* is called.
|
||||
*
|
||||
* The frontend must submit the command buffer before submitting any
|
||||
* other command buffers provided by set_command_buffers. */
|
||||
retro_vulkan_set_command_buffers_t set_command_buffers;
|
||||
|
||||
/* Waits on CPU for device activity for the current sync index to complete.
|
||||
* This is useful since the core will not have a relevant fence to sync with
|
||||
* when the frontend is submitting the command buffers. */
|
||||
retro_vulkan_wait_sync_index_t wait_sync_index;
|
||||
|
||||
/* If the core submits command buffers itself to any of the queues provided
|
||||
* in this interface, the core must lock and unlock the frontend from
|
||||
* racing on the VkQueue.
|
||||
*
|
||||
* Queue submission can happen on any thread.
|
||||
* Even if queue submission happens on the same thread as retro_run(),
|
||||
* the lock/unlock functions must still be called.
|
||||
*
|
||||
* NOTE: Queue submissions are heavy-weight. */
|
||||
retro_vulkan_lock_queue_t lock_queue;
|
||||
retro_vulkan_unlock_queue_t unlock_queue;
|
||||
|
||||
/* Sets a semaphore which is signaled when the image in set_image can safely be reused.
|
||||
* The semaphore is consumed next call to retro_video_refresh_t.
|
||||
* The semaphore will be signalled even for duped frames.
|
||||
* The semaphore will be signalled only once, so set_signal_semaphore should be called every frame.
|
||||
* The semaphore may be VK_NULL_HANDLE, which disables semaphore signalling for next call to retro_video_refresh_t.
|
||||
*
|
||||
* This is mostly useful to support use cases where you're rendering to a single image that
|
||||
* is recycled in a ping-pong fashion with the frontend to save memory (but potentially less throughput).
|
||||
*/
|
||||
retro_vulkan_set_signal_semaphore_t set_signal_semaphore;
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user