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:
2026-07-15 00:35:10 +02:00
parent 1b73361372
commit 4c3b11445c
396 changed files with 140058 additions and 0 deletions
@@ -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()
@@ -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'
}
@@ -0,0 +1 @@
Place {arm64-v8a,armeabi-v7a}/libVkLayer_*.so here to have it bundled in the APK.
@@ -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>
@@ -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$$
}
@@ -0,0 +1 @@
android.useAndroidX=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,3 @@
include 'granite'
project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
include '$$APP$$'
@@ -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$$'
@@ -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
}
@@ -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;
}
}
@@ -0,0 +1,2 @@
<resources>
</resources>
@@ -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>
File diff suppressed because it is too large Load Diff
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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();
}
@@ -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;
}
}
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
@@ -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