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,64 @@
add_granite_internal_lib(granite-application
application.hpp
application_glue.hpp
application.cpp)
if (GRANITE_FFMPEG)
target_link_libraries(granite-application PRIVATE granite-video)
endif()
target_link_libraries(granite-application PUBLIC
granite-vulkan
granite-event
granite-input
granite-application-global-init
granite-application-events
granite-threading
granite-filesystem)
if (NOT (${GRANITE_PLATFORM} MATCHES "null"))
target_sources(granite-application PRIVATE platforms/application_headless.cpp)
target_link_libraries(granite-application PRIVATE granite-stb granite-rapidjson)
endif()
target_include_directories(granite-application PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
if (TARGET granite-renderer)
target_link_libraries(granite-application PUBLIC granite-renderer PRIVATE granite-ui)
target_compile_definitions(granite-application PRIVATE HAVE_GRANITE_RENDERER)
add_granite_internal_lib(granite-scene-viewer scene_viewer_application.cpp scene_viewer_application.hpp)
target_include_directories(granite-scene-viewer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-scene-viewer
PUBLIC granite-scene-export granite-ui granite-application
PRIVATE granite-rapidjson)
endif()
# Can be defined by application to get a custom entry point.
# Otherwise, get a default one.
if (NOT TARGET granite-application-interface-query)
add_granite_internal_lib(granite-application-interface-query STATIC application_interface_query.cpp)
target_compile_options(granite-application-interface-query PRIVATE ${GRANITE_CXX_FLAGS})
endif()
if (NOT ANDROID)
add_granite_internal_lib(granite-application-entry STATIC application_entry.cpp)
target_compile_options(granite-application-entry PRIVATE ${GRANITE_CXX_FLAGS})
target_link_libraries(granite-application-entry
PRIVATE granite-application-interface-query granite-application granite-platform granite-filesystem)
endif()
add_library(granite-application-entry-headless STATIC application_entry.cpp)
target_compile_definitions(granite-application-entry-headless PRIVATE APPLICATION_ENTRY_HEADLESS=1)
target_link_libraries(granite-application-entry-headless
PRIVATE granite-application-interface-query granite-application granite-platform granite-filesystem)
target_compile_options(granite-application-entry-headless PRIVATE ${GRANITE_CXX_FLAGS})
add_subdirectory(events)
add_subdirectory(input)
add_subdirectory(platforms)
add_subdirectory(global)
if (GRANITE_AUDIO)
target_link_libraries(granite-application PRIVATE granite-audio)
endif()
@@ -0,0 +1,344 @@
/* 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 "asset_manager.hpp"
#include "thread_group.hpp"
#ifdef HAVE_GRANITE_RENDERER
#include "material_manager.hpp"
#include "common_renderer_data.hpp"
#endif
#ifdef HAVE_GRANITE_AUDIO
#include "audio_mixer.hpp"
#endif
using namespace Vulkan;
namespace Granite
{
Application::~Application()
{
auto *group = GRANITE_THREAD_GROUP();
if (group)
group->wait_idle();
teardown_wsi();
}
bool Application::init_platform(std::unique_ptr<WSIPlatform> new_platform)
{
#ifdef HAVE_GRANITE_RENDERER
if (auto *common = GRANITE_COMMON_RENDERER_DATA())
common->initialize_static_assets(GRANITE_ASSET_MANAGER(), GRANITE_FILESYSTEM());
#endif
platform = std::move(new_platform);
application_wsi.set_platform(platform.get());
if (auto *event = GRANITE_EVENT_MANAGER())
event->enqueue_latched<ApplicationWSIPlatformEvent>(*platform);
return true;
}
void Application::teardown_wsi()
{
if (auto *event = GRANITE_EVENT_MANAGER())
{
event->dequeue_all_latched(DevicePipelineReadyEvent::get_type_id());
event->dequeue_all_latched(DeviceShaderModuleReadyEvent::get_type_id());
event->dequeue_all_latched(ApplicationWSIPlatformEvent::get_type_id());
}
application_wsi.teardown();
ready_modules = false;
ready_pipelines = false;
}
bool Application::init_wsi(Vulkan::ContextHandle context)
{
if (context)
{
if (!application_wsi.init_from_existing_context(std::move(context)))
return false;
}
else
{
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();
if (!application_wsi.init_context_from_platform(
system_handles.thread_group->get_num_threads() + 1,
system_handles))
{
return false;
}
}
if (!application_wsi.init_device())
return false;
application_wsi.get_device().begin_shader_caches();
{
GRANITE_SCOPED_TIMELINE_EVENT("wsi-init-swapchain");
if (!platform->has_external_swapchain() && !application_wsi.init_surface_swapchain())
return false;
}
return true;
}
void Application::poll_input_tracker_async(Granite::InputTrackerHandler *override_handler)
{
get_platform().poll_input_async(override_handler);
}
bool Application::poll()
{
auto &wsi = get_wsi();
if (!get_platform().alive(wsi))
return false;
if (requested_shutdown)
return false;
auto *fs = GRANITE_FILESYSTEM();
auto *em = GRANITE_EVENT_MANAGER();
if (fs)
fs->poll_notifications();
if (em)
em->dispatch();
#ifdef HAVE_GRANITE_AUDIO
auto *backend = GRANITE_AUDIO_BACKEND();
if (backend)
backend->heartbeat();
auto *am = GRANITE_AUDIO_MIXER();
if (am)
{
// Pump through events from audio thread.
auto &queue = am->get_message_queue();
Util::MessageQueuePayload payload;
while ((payload = queue.read_message()))
{
auto &event = payload.as<Event>();
if (em)
em->dispatch_inline(event);
queue.recycle_payload(std::move(payload));
}
// Recycle dead streams.
am->dispose_dead_streams();
}
#endif
return true;
}
void Application::check_initialization_progress()
{
auto &device = get_wsi().get_device();
if (!ready_modules)
{
if (device.query_initialization_progress(Device::InitializationStage::CacheMaintenance) >= 100 &&
device.query_initialization_progress(Device::InitializationStage::ShaderModules) >= 100)
{
if (auto *manager = GRANITE_ASSET_MANAGER())
{
// Now is a good time to kick shader manager since it might require compute shaders for decode.
manager->iterate(GRANITE_THREAD_GROUP());
}
if (auto *event = GRANITE_EVENT_MANAGER())
{
GRANITE_SCOPED_TIMELINE_EVENT("dispatch-ready-modules");
#ifdef HAVE_GRANITE_RENDERER
auto *manager = &device.get_shader_manager();
#else
constexpr Vulkan::ShaderManager *manager = nullptr;
#endif
event->enqueue_latched<DeviceShaderModuleReadyEvent>(&device, manager);
}
ready_modules = true;
}
}
if (!ready_pipelines)
{
if (device.query_initialization_progress(Device::InitializationStage::Pipelines) >= 100)
{
if (auto *event = GRANITE_EVENT_MANAGER())
{
GRANITE_SCOPED_TIMELINE_EVENT("dispatch-ready-pipelines");
#ifdef HAVE_GRANITE_RENDERER
auto *manager = &device.get_shader_manager();
#else
constexpr Vulkan::ShaderManager *manager = nullptr;
#endif
event->enqueue_latched<DevicePipelineReadyEvent>(&device, manager);
}
ready_pipelines = true;
}
}
}
void Application::show_message_box(const std::string &str, Vulkan::WSIPlatform::MessageType type)
{
if (platform)
platform->show_message_box(str, type);
switch (type)
{
case Vulkan::WSIPlatform::MessageType::Error:
LOGE("%s\n", str.c_str());
break;
case Vulkan::WSIPlatform::MessageType::Warning:
LOGW("%s\n", str.c_str());
break;
case Vulkan::WSIPlatform::MessageType::Info:
LOGI("%s\n", str.c_str());
break;
}
}
void Application::run_frame()
{
check_initialization_progress();
{
GRANITE_SCOPED_TIMELINE_EVENT("wsi-begin-frame");
if (!application_wsi.begin_frame())
{
LOGE("Failed to begin frame. Fatal error. Shutting down.\n");
request_shutdown();
return;
}
}
double smooth_frame_time = application_wsi.get_smooth_frame_time();
double smooth_elapsed = application_wsi.get_smooth_elapsed_time();
if (!ready_modules)
{
GRANITE_SCOPED_TIMELINE_EVENT("render-early-loading");
render_early_loading(smooth_frame_time, smooth_elapsed);
}
else if (!ready_pipelines)
{
GRANITE_SCOPED_TIMELINE_EVENT("render-loading");
render_loading(smooth_frame_time, smooth_elapsed);
}
else
{
GRANITE_SCOPED_TIMELINE_EVENT("render-frame");
render_frame(smooth_frame_time, smooth_elapsed);
}
{
GRANITE_SCOPED_TIMELINE_EVENT("wsi-end-frame");
application_wsi.end_frame();
}
{
GRANITE_SCOPED_TIMELINE_EVENT("post-frame");
post_frame();
}
}
void Application::render_early_loading(double, double)
{
auto &device = application_wsi.get_device();
auto cmd = device.request_command_buffer();
auto rp = device.get_swapchain_render_pass(SwapchainRenderPass::ColorOnly);
rp.clear_color[0].float32[0] = 0.01f;
rp.clear_color[0].float32[2] = 0.02f;
rp.clear_color[0].float32[3] = 0.03f;
cmd->begin_render_pass(rp);
auto vp = cmd->get_viewport();
VkClearRect rect = {};
rect.layerCount = 1;
rect.rect.extent = {
uint32_t(vp.width * 0.01f * float(device.query_initialization_progress(Device::InitializationStage::ShaderModules))),
uint32_t(vp.height),
};
VkClearValue value = {};
value.color.float32[0] = 0.08f;
value.color.float32[1] = 0.01f;
value.color.float32[2] = 0.01f;
if (rect.rect.extent.width > 0)
cmd->clear_quad(0, rect, value);
cmd->end_render_pass();
device.submit(cmd);
}
void Application::render_loading(double, double)
{
auto &device = application_wsi.get_device();
auto cmd = device.request_command_buffer();
auto rp = device.get_swapchain_render_pass(SwapchainRenderPass::ColorOnly);
rp.clear_color[0].float32[0] = 0.01f;
rp.clear_color[0].float32[2] = 0.02f;
rp.clear_color[0].float32[3] = 0.03f;
cmd->begin_render_pass(rp);
auto vp = cmd->get_viewport();
VkClearRect rect = {};
rect.layerCount = 1;
rect.rect.extent = {
uint32_t(vp.width * 0.01f * float(device.query_initialization_progress(Device::InitializationStage::Pipelines))),
uint32_t(vp.height),
};
VkClearValue value = {};
value.color.float32[0] = 0.01f;
value.color.float32[1] = 0.08f;
value.color.float32[2] = 0.01f;
if (rect.rect.extent.width > 0)
cmd->clear_quad(0, rect, value);
cmd->end_render_pass();
device.submit(cmd);
}
void Application::post_frame()
{
// Texture manager might require shaders to be ready before we can submit work.
if (ready_modules)
{
if (auto *manager = GRANITE_ASSET_MANAGER())
manager->iterate(GRANITE_THREAD_GROUP());
}
if (auto *manager = Global::material_manager())
manager->iterate(Global::asset_manager());
}
}
@@ -0,0 +1,106 @@
/* 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 "wsi.hpp"
#include "application_wsi_events.hpp"
#include "input.hpp"
#include "application_glue.hpp"
namespace Granite
{
class Application
{
public:
virtual ~Application();
virtual void render_frame(double frame_time, double elapsed_time) = 0;
bool init_platform(std::unique_ptr<Vulkan::WSIPlatform> platform);
bool init_wsi(Vulkan::ContextHandle context = {});
void teardown_wsi();
// Called after the frame is submitted for presentation.
// Can do "garbage collection" or similar batched cleanup
// that does not depend on submitting more graphics work.
virtual void post_frame();
// In early loading, we have not loaded SPIR-V yet.
// Rendering a background color or extremely basic shaders could work here.
// If compiling without SPIRV-Cross or compiler support in a shipping configuration,
// any SPIR-V must be provided inline through slangmosh or similar.
virtual void render_early_loading(double frame_time, double elapsed_time);
// In loading, we have access to SPIR-V, but compiling pipelines is not done yet.
// This stage is more suited for rendering splash screens or similar.
virtual void render_loading(double frame_time, double elapsed_time);
Vulkan::WSI &get_wsi()
{
return application_wsi;
}
Vulkan::WSIPlatform &get_platform()
{
return *platform;
}
virtual std::string get_name()
{
return "granite";
}
virtual unsigned get_version()
{
return 0;
}
virtual unsigned get_default_width()
{
return 1280;
}
virtual unsigned get_default_height()
{
return 720;
}
bool poll();
void run_frame();
void show_message_box(const std::string &str, Vulkan::WSIPlatform::MessageType type);
protected:
void request_shutdown()
{
requested_shutdown = true;
}
void poll_input_tracker_async(InputTrackerHandler *override_handler);
private:
std::unique_ptr<Vulkan::WSIPlatform> platform;
Vulkan::WSI application_wsi;
bool requested_shutdown = false;
// Ready state for deferred device initialization.
bool ready_modules = false;
bool ready_pipelines = false;
void check_initialization_progress();
};
}
@@ -0,0 +1,100 @@
/* 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.hpp"
#include "filesystem.hpp"
#include "path_utils.hpp"
//#define USE_FP_EXCEPTIONS
#ifdef USE_FP_EXCEPTIONS
#include <fenv.h>
#endif
#if defined(_WIN32) && !defined(APPLICATION_ENTRY_HEADLESS)
#define USE_WINMAIN
#endif
#ifdef USE_WINMAIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <vector>
#endif
namespace Granite
{
// Make sure this is linked in.
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);
}
}
#ifdef USE_WINMAIN
int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
#else
int main(int argc, char *argv[])
#endif
{
#ifdef USE_FP_EXCEPTIONS
feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW);
#endif
#ifdef USE_WINMAIN
int argc;
wchar_t **wide_argv = CommandLineToArgvW(GetCommandLineW(), &argc);
std::vector<char *> argv_buffer(argc + 1);
char **argv = nullptr;
std::vector<std::string> argv_strings(argc);
if (wide_argv)
{
argv = argv_buffer.data();
for (int i = 0; i < argc; i++)
{
argv_strings[i] = Granite::Path::to_utf8(wide_argv[i]);
argv_buffer[i] = const_cast<char *>(argv_strings[i].c_str());
}
}
#endif
#ifdef APPLICATION_ENTRY_HEADLESS
int ret = Granite::application_main_headless(Granite::query_application_interface,
Granite::application_create,
argc, argv);
#else
int ret = Granite::application_main(Granite::query_application_interface,
Granite::application_create,
argc, argv);
#endif
return ret;
}
@@ -0,0 +1,68 @@
/* 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 <stddef.h>
#include <stdint.h>
namespace Granite
{
enum class ApplicationQuery
{
DefaultManagerFlags
};
class Application;
int application_main(
bool (*query_application_interface)(ApplicationQuery, void *data, size_t size),
Application *(*create_application)(int, char **),
int argc, char **argv);
int application_main_headless(
bool (*query_application_interface)(ApplicationQuery, void *data, size_t size),
Application *(*create_application)(int, char **),
int argc, char **argv);
extern Application *application_create(int argc, char *argv[]);
struct ApplicationQueryDefaultManagerFlags
{
uint32_t manager_feature_flags;
};
extern bool query_application_interface(ApplicationQuery query, void *data, size_t size);
// Call this or setup_default_filesystem to ensure application-main is linked in correctly without having to mess around
// with -Wl,--whole-archive.
void application_dummy();
void application_setup_default_filesystem(const char *default_asset_directory);
}
#ifdef ASSET_DIRECTORY
#define GRANITE_APPLICATION_SETUP_FILESYSTEM() ::Granite::application_setup_default_filesystem(ASSET_DIRECTORY)
#else
#define GRANITE_APPLICATION_SETUP_FILESYSTEM() ::Granite::application_setup_default_filesystem(nullptr)
#endif
#define GRANITE_APPLICATION_DECL_DEFAULT_QUERY() namespace Granite { bool query_application_interface(Granite::ApplicationQuery, void *, size_t) { return false; } }
@@ -0,0 +1,2 @@
#include "application_glue.hpp"
GRANITE_APPLICATION_DECL_DEFAULT_QUERY()
@@ -0,0 +1,7 @@
add_granite_internal_lib(granite-application-events
application_wsi.hpp
application_wsi.cpp application_wsi_events.hpp
application_events.hpp)
target_include_directories(granite-application-events PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-events
PRIVATE granite-vulkan granite-event granite-input granite-application-global)
@@ -0,0 +1,55 @@
/* 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 "event.hpp"
namespace Granite
{
enum class ApplicationLifecycle
{
Running,
Paused,
Stopped,
Dead
};
class ApplicationLifecycleEvent : public Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationLifecycleEvent)
explicit ApplicationLifecycleEvent(ApplicationLifecycle lifecycle_)
: lifecycle(lifecycle_)
{
}
ApplicationLifecycle get_lifecycle() const
{
return lifecycle;
}
private:
ApplicationLifecycle lifecycle;
};
}
@@ -0,0 +1,188 @@
/* 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_wsi.hpp"
#include "application_wsi_events.hpp"
#include "application_events.hpp"
#include "global_managers.hpp"
#include "event.hpp"
namespace Granite
{
GraniteWSIPlatform::GraniteWSIPlatform()
{
input_tracker.set_input_handler(this);
}
void GraniteWSIPlatform::event_swapchain_created(Vulkan::Device *device, VkSwapchainKHR, unsigned width, unsigned height,
float aspect_ratio, size_t image_count, VkFormat format,
VkColorSpaceKHR color_space,
VkSurfaceTransformFlagBitsKHR transform)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->enqueue_latched<Vulkan::SwapchainParameterEvent>(device, width, height, aspect_ratio, image_count, format,
color_space, transform);
}
}
void GraniteWSIPlatform::event_swapchain_destroyed()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->dequeue_all_latched(Vulkan::SwapchainParameterEvent::get_type_id());
}
void GraniteWSIPlatform::event_device_created(Vulkan::Device *device)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->enqueue_latched<Vulkan::DeviceCreatedEvent>(device);
}
void GraniteWSIPlatform::event_device_destroyed()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->dequeue_all_latched(Vulkan::DeviceCreatedEvent::get_type_id());
}
void GraniteWSIPlatform::event_swapchain_index(Vulkan::Device *device, unsigned index)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(Vulkan::SwapchainIndexEvent::get_type_id());
em->enqueue_latched<Vulkan::SwapchainIndexEvent>(device, index);
}
}
void GraniteWSIPlatform::event_frame_tick(double, double)
{
}
template <typename T>
void GraniteWSIPlatform::dispatch_template(const T &t)
{
if (auto *em = GRANITE_EVENT_MANAGER())
em->dispatch_inline(t);
}
template <typename T>
void GraniteWSIPlatform::dispatch_template_filter(const T &t)
{
auto *ui = Global::ui_manager();
if (ui && !ui->filter_input_event(t))
return;
dispatch_template(t);
}
template <typename Func>
void GraniteWSIPlatform::dispatch_or_defer(Func &&func)
{
if (in_async_input)
captured.emplace_back(std::forward<Func>(func));
else
func();
}
#define WORK(work) dispatch_or_defer([this, e]() { work; })
void GraniteWSIPlatform::dispatch(const TouchDownEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const TouchUpEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const TouchGestureEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadButtonEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadAxisEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const KeyboardEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const OrientationEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const MouseButtonEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const MouseMoveEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadStateEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::dispatch(const InputStateEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::dispatch(const JoypadConnectionEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::begin_async_input_handling()
{
in_async_input = true;
}
void GraniteWSIPlatform::end_async_input_handling()
{
in_async_input = false;
}
void GraniteWSIPlatform::flush_deferred_input_events()
{
VK_ASSERT(!in_async_input);
for (auto &func : captured)
func();
captured.clear();
}
}
@@ -0,0 +1,81 @@
/* 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 "wsi.hpp"
#include "input.hpp"
#include <functional>
namespace Granite
{
class GraniteWSIPlatform : public Vulkan::WSIPlatform, private InputTrackerHandler
{
public:
GraniteWSIPlatform();
InputTracker &get_input_tracker()
{
return input_tracker;
}
protected:
void event_device_created(Vulkan::Device *device) override;
void event_device_destroyed() override;
void event_swapchain_created(Vulkan::Device *device, VkSwapchainKHR swapchain,
unsigned width, unsigned height,
float aspect_ratio, size_t image_count,
VkFormat format, VkColorSpaceKHR color_space,
VkSurfaceTransformFlagBitsKHR pre_rotate) override;
void event_swapchain_destroyed() override;
void event_swapchain_index(Vulkan::Device *device, unsigned index) override;
void event_frame_tick(double frame, double elapsed) override;
void begin_async_input_handling();
void end_async_input_handling();
void flush_deferred_input_events();
private:
InputTracker input_tracker;
void dispatch(const TouchDownEvent &e) override;
void dispatch(const TouchUpEvent &e) override;
void dispatch(const TouchGestureEvent &e) override;
void dispatch(const JoypadButtonEvent &e) override;
void dispatch(const JoypadAxisEvent &e) override;
void dispatch(const KeyboardEvent &e) override;
void dispatch(const OrientationEvent &e) override;
void dispatch(const MouseButtonEvent &e) override;
void dispatch(const MouseMoveEvent &e) override;
void dispatch(const JoypadStateEvent &e) override;
void dispatch(const InputStateEvent &e) override;
void dispatch(const JoypadConnectionEvent &e) override;
template <typename T>
void dispatch_template_filter(const T &t);
template <typename T>
void dispatch_template(const T &t);
template <typename Func>
void dispatch_or_defer(Func &&func);
bool in_async_input = false;
std::vector<std::function<void ()>> captured;
};
}
@@ -0,0 +1,258 @@
/* 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 "event.hpp"
#include "vulkan_headers.hpp"
namespace Vulkan
{
class Device;
class ShaderManager;
class WSIPlatform;
class DeviceCreatedEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DeviceCreatedEvent)
explicit DeviceCreatedEvent(Device *device_)
: device(*device_)
{}
Device &get_device() const
{
return device;
}
private:
Device &device;
};
class DeviceShaderModuleReadyEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DeviceShaderModuleReadyEvent)
explicit DeviceShaderModuleReadyEvent(Device *device_, ShaderManager *manager_)
: device(*device_), manager(*manager_)
{}
Device &get_device() const
{
return device;
}
ShaderManager &get_shader_manager() const
{
return manager;
}
private:
Device &device;
ShaderManager &manager;
};
class DevicePipelineReadyEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DevicePipelineReadyEvent)
explicit DevicePipelineReadyEvent(Device *device_, ShaderManager *manager_)
: device(*device_), manager(*manager_)
{}
Device &get_device() const
{
return device;
}
ShaderManager &get_shader_manager() const
{
return manager;
}
private:
Device &device;
ShaderManager &manager;
};
class SwapchainParameterEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(SwapchainParameterEvent)
SwapchainParameterEvent(Device *device_,
unsigned width_, unsigned height_,
float aspect_ratio_, unsigned count_,
VkFormat format_, VkColorSpaceKHR color_space_,
VkSurfaceTransformFlagBitsKHR transform_)
: device(*device_), width(width_), height(height_),
aspect_ratio(aspect_ratio_), image_count(count_), format(format_), color_space(color_space_), transform(transform_)
{}
Device &get_device() const
{
return device;
}
unsigned get_width() const
{
return width;
}
unsigned get_height() const
{
return height;
}
float get_aspect_ratio() const
{
return aspect_ratio;
}
unsigned get_image_count() const
{
return image_count;
}
VkFormat get_format() const
{
return format;
}
VkColorSpaceKHR get_color_space() const
{
return color_space;
}
VkSurfaceTransformFlagBitsKHR get_prerotate() const
{
return transform;
}
private:
Device &device;
unsigned width;
unsigned height;
float aspect_ratio;
unsigned image_count;
VkFormat format;
VkColorSpaceKHR color_space;
VkSurfaceTransformFlagBitsKHR transform;
};
class SwapchainIndexEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(SwapchainIndexEvent)
SwapchainIndexEvent(Device *device_, unsigned index_)
: device(*device_), index(index_)
{}
Device &get_device() const
{
return device;
}
unsigned get_index() const
{
return index;
}
private:
Device &device;
unsigned index;
};
class ApplicationWSIPlatformEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWSIPlatformEvent)
explicit ApplicationWSIPlatformEvent(WSIPlatform &platform_)
: platform(platform_)
{}
WSIPlatform &get_platform() const
{
return platform;
}
private:
WSIPlatform &platform;
};
class ApplicationWindowFileDropEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWindowFileDropEvent)
explicit ApplicationWindowFileDropEvent(std::string path_)
: path(std::move(path_))
{}
const std::string &get_path() const
{
return path;
}
private:
std::string path;
};
class ApplicationWindowTextDropEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWindowTextDropEvent)
explicit ApplicationWindowTextDropEvent(std::string str_)
: str(std::move(str_))
{}
const std::string &get_text() const
{
return str;
}
private:
std::string str;
};
class ApplicationSoftKeyboardUpdateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationSoftKeyboardUpdateEvent)
explicit ApplicationSoftKeyboardUpdateEvent(std::string str_)
: str(std::move(str_))
{
}
const std::string &get_text() const
{
return str;
}
private:
std::string str;
};
}
@@ -0,0 +1,27 @@
add_granite_internal_lib(granite-application-global
global_managers.hpp global_managers.cpp)
target_include_directories(granite-application-global PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-global PUBLIC granite-util)
add_granite_internal_lib(granite-application-global-init
global_managers_init.hpp global_managers_init.cpp)
target_include_directories(granite-application-global-init PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-global-init
PUBLIC granite-application-global
PRIVATE granite-threading granite-event granite-filesystem)
if (TARGET granite-renderer)
target_link_libraries(granite-application-global-init PRIVATE granite-renderer granite-ui)
target_compile_definitions(granite-application-global-init PRIVATE HAVE_GRANITE_RENDERER)
endif()
if (GRANITE_AUDIO)
target_link_libraries(granite-application-global-init PRIVATE granite-audio)
endif()
if (GRANITE_BULLET)
target_link_libraries(granite-application-global-init PRIVATE granite-physics)
endif()
add_library(granite-application-global-interface INTERFACE)
target_include_directories(granite-application-global-interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
@@ -0,0 +1,308 @@
/* 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 "global_managers.hpp"
#include "environment.hpp"
#include "logging.hpp"
#include <thread>
#include <assert.h>
#include <stdlib.h>
namespace Granite
{
namespace Global
{
// Could use unique_ptr here, but would be nice to avoid global ctor/dtor.
struct GlobalManagers
{
Factory *factory;
FilesystemInterface *filesystem;
AssetManagerInterface *asset_manager;
MaterialManagerInterface *material_manager;
EventManagerInterface *event_manager;
ThreadGroupInterface *thread_group;
UI::UIManagerInterface *ui_manager;
CommonRendererDataInterface *common_renderer_data;
Util::MessageQueueInterface *logging;
Audio::BackendInterface *audio_backend;
Audio::MixerInterface *audio_mixer;
PhysicsSystemInterface *physics;
};
static thread_local GlobalManagers global_managers;
GlobalManagersHandle create_thread_context()
{
return GlobalManagersHandle(new GlobalManagers(global_managers));
}
void delete_thread_context(GlobalManagers *managers)
{
delete managers;
}
void GlobalManagerDeleter::operator()(GlobalManagers *managers)
{
delete_thread_context(managers);
}
void set_thread_context(const GlobalManagers &managers)
{
global_managers = managers;
if (managers.thread_group)
managers.thread_group->set_thread_context();
if (managers.logging)
Util::set_thread_logging_interface(managers.logging);
}
void clear_thread_context()
{
global_managers = {};
}
Util::MessageQueueInterface *message_queue()
{
return global_managers.logging;
}
FilesystemInterface *filesystem()
{
return global_managers.filesystem;
}
AssetManagerInterface *asset_manager()
{
return global_managers.asset_manager;
}
MaterialManagerInterface *material_manager()
{
return global_managers.material_manager;
}
EventManagerInterface *event_manager()
{
return global_managers.event_manager;
}
ThreadGroupInterface *thread_group()
{
return global_managers.thread_group;
}
UI::UIManagerInterface *ui_manager()
{
return global_managers.ui_manager;
}
CommonRendererDataInterface *common_renderer_data()
{
return global_managers.common_renderer_data;
}
Audio::BackendInterface *audio_backend() { return global_managers.audio_backend; }
Audio::MixerInterface *audio_mixer() { return global_managers.audio_mixer; }
void install_audio_system(Audio::BackendInterface *backend, Audio::MixerInterface *mixer)
{
delete global_managers.audio_mixer;
global_managers.audio_mixer = mixer;
delete global_managers.audio_backend;
global_managers.audio_backend = backend;
}
PhysicsSystemInterface *physics()
{
return global_managers.physics;
}
void init(Factory &factory, ManagerFeatureFlags flags, unsigned max_threads, float audio_sample_rate)
{
assert(!global_managers.factory || global_managers.factory == &factory);
global_managers.factory = &factory;
if (flags & MANAGER_FEATURE_EVENT_BIT)
{
if (!global_managers.event_manager)
global_managers.event_manager = factory.create_event_manager();
}
if (flags & MANAGER_FEATURE_FILESYSTEM_BIT)
{
if (!global_managers.filesystem)
global_managers.filesystem = factory.create_filesystem();
}
if (flags & MANAGER_FEATURE_ASSET_MANAGER_BIT)
{
if (!global_managers.asset_manager)
global_managers.asset_manager = factory.create_asset_manager();
}
if (flags & MANAGER_FEATURE_MATERIAL_MANAGER_BIT)
{
if (!global_managers.material_manager)
global_managers.material_manager = factory.create_material_manager();
}
bool kick_threads = false;
if (flags & MANAGER_FEATURE_THREAD_GROUP_BIT)
{
if (!global_managers.thread_group)
{
global_managers.thread_group = factory.create_thread_group();
kick_threads = true;
}
}
if (flags & MANAGER_FEATURE_UI_MANAGER_BIT)
{
if (!global_managers.ui_manager)
global_managers.ui_manager = factory.create_ui_manager();
}
if (flags & MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT)
{
if (!global_managers.common_renderer_data)
global_managers.common_renderer_data = factory.create_common_renderer_data();
}
if (flags & MANAGER_FEATURE_LOGGING_BIT)
{
if (!global_managers.logging)
global_managers.logging = factory.create_message_queue();
Util::set_thread_logging_interface(global_managers.logging);
}
if (flags & MANAGER_FEATURE_PHYSICS_BIT)
{
if (!global_managers.physics)
global_managers.physics = factory.create_physics_system();
}
if (flags & MANAGER_FEATURE_AUDIO_MIXER_BIT)
{
if (!global_managers.audio_mixer)
global_managers.audio_mixer = factory.create_audio_mixer();
}
if (flags & MANAGER_FEATURE_AUDIO_BACKEND_BIT)
{
if (!global_managers.audio_backend)
global_managers.audio_backend = factory.create_audio_backend(global_managers.audio_mixer, audio_sample_rate, 2);
}
// Kick threads after all global managers are set up.
if (kick_threads)
{
unsigned cpu_threads = std::thread::hardware_concurrency();
cpu_threads = cpu_threads > 1 ? (cpu_threads - 1u) : 1u;
if (cpu_threads > max_threads)
cpu_threads = max_threads;
cpu_threads = Util::get_environment_uint("GRANITE_NUM_WORKER_THREADS", cpu_threads);
unsigned background_cpu_threads = (cpu_threads + 1) / 2;
global_managers.thread_group->start(cpu_threads, background_cpu_threads,
[ctx = std::shared_ptr<GlobalManagers>(create_thread_context())] {
set_thread_context(*ctx);
});
}
}
void deinit()
{
if (!global_managers.factory)
return;
if (global_managers.audio_backend)
global_managers.audio_backend->stop();
delete global_managers.audio_backend;
delete global_managers.audio_mixer;
delete global_managers.physics;
delete global_managers.common_renderer_data;
delete global_managers.ui_manager;
delete global_managers.thread_group;
delete global_managers.material_manager;
delete global_managers.asset_manager;
delete global_managers.filesystem;
delete global_managers.event_manager;
delete global_managers.logging;
global_managers.audio_backend = nullptr;
global_managers.audio_mixer = nullptr;
global_managers.physics = nullptr;
global_managers.common_renderer_data = nullptr;
global_managers.filesystem = nullptr;
global_managers.material_manager = nullptr;
global_managers.asset_manager = nullptr;
global_managers.event_manager = nullptr;
global_managers.thread_group = nullptr;
global_managers.ui_manager = nullptr;
global_managers.logging = nullptr;
global_managers.factory = nullptr;
}
void start_audio_system()
{
if (!global_managers.audio_backend)
return;
if (!global_managers.audio_backend->start())
{
LOGE("Failed to start audio subsystem!\n");
return;
}
if (global_managers.event_manager && global_managers.audio_mixer)
global_managers.audio_mixer->event_start(*global_managers.event_manager);
}
void stop_audio_system()
{
if (!global_managers.audio_backend)
return;
if (!global_managers.audio_backend->stop())
LOGE("Failed to stop audio subsystem!\n");
if (global_managers.event_manager && global_managers.audio_mixer)
global_managers.audio_mixer->event_stop(*global_managers.event_manager);
}
FilesystemInterface *Factory::create_filesystem() { return nullptr; }
AssetManagerInterface *Factory::create_asset_manager() { return nullptr; }
MaterialManagerInterface *Factory::create_material_manager() { return nullptr; }
EventManagerInterface *Factory::create_event_manager() { return nullptr; }
ThreadGroupInterface *Factory::create_thread_group() { return nullptr; }
CommonRendererDataInterface *Factory::create_common_renderer_data() { return nullptr; }
PhysicsSystemInterface *Factory::create_physics_system() { return nullptr; }
Audio::BackendInterface *Factory::create_audio_backend(Audio::MixerInterface *, float, unsigned) { return nullptr; }
Audio::MixerInterface *Factory::create_audio_mixer() { return nullptr; }
UI::UIManagerInterface *Factory::create_ui_manager() { return nullptr; }
Util::MessageQueueInterface *Factory::create_message_queue() { return nullptr; }
}
}
@@ -0,0 +1,127 @@
/* 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 <stdint.h>
#include <functional>
#include <memory>
#include <limits.h>
#include "global_managers_interface.hpp"
namespace Granite
{
namespace Global
{
enum ManagerFeatureFlagBits
{
MANAGER_FEATURE_FILESYSTEM_BIT = 1 << 0,
MANAGER_FEATURE_EVENT_BIT = 1 << 1,
MANAGER_FEATURE_THREAD_GROUP_BIT = 1 << 2,
MANAGER_FEATURE_UI_MANAGER_BIT = 1 << 3,
MANAGER_FEATURE_AUDIO_MIXER_BIT = 1 << 4,
MANAGER_FEATURE_AUDIO_BACKEND_BIT = 1 << 5,
MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT = 1 << 6,
MANAGER_FEATURE_PHYSICS_BIT = 1 << 7,
MANAGER_FEATURE_LOGGING_BIT = 1 << 8,
MANAGER_FEATURE_ASSET_MANAGER_BIT = 1 << 9,
MANAGER_FEATURE_MATERIAL_MANAGER_BIT = 1 << 10,
MANAGER_FEATURE_DEFAULT_BITS = (MANAGER_FEATURE_FILESYSTEM_BIT |
MANAGER_FEATURE_ASSET_MANAGER_BIT |
MANAGER_FEATURE_MATERIAL_MANAGER_BIT |
MANAGER_FEATURE_EVENT_BIT |
MANAGER_FEATURE_THREAD_GROUP_BIT |
MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT |
MANAGER_FEATURE_UI_MANAGER_BIT |
MANAGER_FEATURE_AUDIO_MIXER_BIT |
MANAGER_FEATURE_AUDIO_BACKEND_BIT)
};
using ManagerFeatureFlags = uint32_t;
// Decouple creation from global TLS storage.
// This avoids some nasty cyclical dependencies.
class Factory
{
public:
virtual ~Factory() = default;
virtual FilesystemInterface *create_filesystem();
virtual AssetManagerInterface *create_asset_manager();
virtual MaterialManagerInterface *create_material_manager();
virtual EventManagerInterface *create_event_manager();
virtual ThreadGroupInterface *create_thread_group();
virtual CommonRendererDataInterface *create_common_renderer_data();
virtual PhysicsSystemInterface *create_physics_system();
virtual Audio::BackendInterface *create_audio_backend(Audio::MixerInterface *mixer,
float sample_rate,
unsigned channels);
virtual Audio::MixerInterface *create_audio_mixer();
virtual UI::UIManagerInterface *create_ui_manager();
virtual Util::MessageQueueInterface *create_message_queue();
};
void init(Factory &factory, ManagerFeatureFlags flags = MANAGER_FEATURE_DEFAULT_BITS,
unsigned max_threads = UINT_MAX, float audio_sample_rate = -1.0f);
void deinit();
// Used if the application wants to use multiple instances of Granite in the same process.
// This allows each thread to be associated to a global context.
struct GlobalManagers;
struct GlobalManagerDeleter
{
void operator()(GlobalManagers *managers);
};
using GlobalManagersHandle = std::unique_ptr<GlobalManagers, GlobalManagerDeleter>;
GlobalManagersHandle create_thread_context();
void delete_thread_context(GlobalManagers *managers);
void set_thread_context(const GlobalManagers &managers);
void clear_thread_context();
void start_audio_system();
void stop_audio_system();
void install_audio_system(Audio::BackendInterface *backend, Audio::MixerInterface *mixer);
Util::MessageQueueInterface *message_queue();
FilesystemInterface *filesystem();
AssetManagerInterface *asset_manager();
MaterialManagerInterface *material_manager();
EventManagerInterface *event_manager();
ThreadGroupInterface *thread_group();
UI::UIManagerInterface *ui_manager();
CommonRendererDataInterface *common_renderer_data();
Audio::BackendInterface *audio_backend();
Audio::MixerInterface *audio_mixer();
PhysicsSystemInterface *physics();
}
}
#define GRANITE_MESSAGE_QUEUE() static_cast<::Util::MessageQueue *>(::Granite::Global::message_queue())
#define GRANITE_FILESYSTEM() static_cast<::Granite::Filesystem *>(::Granite::Global::filesystem())
#define GRANITE_ASSET_MANAGER() static_cast<::Granite::AssetManager *>(::Granite::Global::asset_manager())
#define GRANITE_MATERIAL_MANAGER() static_cast<::Granite::MaterialManager *>(::Granite::Global::material_manager())
#define GRANITE_EVENT_MANAGER() static_cast<::Granite::EventManager *>(::Granite::Global::event_manager())
#define GRANITE_THREAD_GROUP() static_cast<::Granite::ThreadGroup *>(::Granite::Global::thread_group())
#define GRANITE_UI_MANAGER() static_cast<::Granite::UI::UIManager *>(::Granite::Global::ui_manager())
#define GRANITE_COMMON_RENDERER_DATA() static_cast<::Granite::CommonRendererData *>(::Granite::Global::common_renderer_data())
#define GRANITE_AUDIO_BACKEND() static_cast<::Granite::Audio::Backend *>(::Granite::Global::audio_backend())
#define GRANITE_AUDIO_MIXER() static_cast<::Granite::Audio::Mixer *>(::Granite::Global::audio_mixer())
#define GRANITE_PHYSICS() static_cast<::Granite::PhysicsSystem *>(::Granite::Global::physics())
@@ -0,0 +1,137 @@
/* 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 "global_managers_init.hpp"
#include "global_managers.hpp"
#include "event.hpp"
#include "thread_group.hpp"
#include "filesystem.hpp"
#include "asset_manager.hpp"
#ifdef HAVE_GRANITE_RENDERER
#include "material_manager.hpp"
#include "common_renderer_data.hpp"
#include "ui_manager.hpp"
#endif
#ifdef HAVE_GRANITE_AUDIO
#include "audio_mixer.hpp"
#include "audio_interface.hpp"
#endif
#ifdef HAVE_GRANITE_PHYSICS
#include "physics_system.hpp"
#endif
namespace Granite
{
namespace Global
{
struct FactoryImplementation : Factory
{
FilesystemInterface *create_filesystem() override
{
return new Filesystem;
}
AssetManagerInterface *create_asset_manager() override
{
return new AssetManager;
}
EventManagerInterface *create_event_manager() override
{
return new EventManager;
}
ThreadGroupInterface *create_thread_group() override
{
return new ThreadGroup;
}
CommonRendererDataInterface *create_common_renderer_data() override
{
#ifdef HAVE_GRANITE_RENDERER
return new CommonRendererData;
#else
return nullptr;
#endif
}
UI::UIManagerInterface *create_ui_manager() override
{
#ifdef HAVE_GRANITE_RENDERER
return new UI::UIManager;
#else
return nullptr;
#endif
}
MaterialManagerInterface *create_material_manager() override
{
#ifdef HAVE_GRANITE_RENDERER
return new MaterialManager;
#else
return nullptr;
#endif
}
Audio::MixerInterface *create_audio_mixer() override
{
#ifdef HAVE_GRANITE_AUDIO
return new Audio::Mixer;
#else
return nullptr;
#endif
}
Audio::BackendInterface *create_audio_backend(Audio::MixerInterface *iface, float sample_rate, unsigned channels) override
{
#ifdef HAVE_GRANITE_AUDIO
if (iface)
return Audio::create_default_audio_backend(static_cast<Audio::Mixer *>(iface), sample_rate, channels);
else
return nullptr;
#else
(void)iface;
(void)sample_rate;
(void)channels;
return nullptr;
#endif
}
PhysicsSystemInterface *create_physics_system() override
{
#ifdef HAVE_GRANITE_PHYSICS
return new PhysicsSystem;
#else
return nullptr;
#endif
}
};
static FactoryImplementation factory;
void init(ManagerFeatureFlags flags, unsigned max_threads, float audio_sample_rate)
{
init(factory, flags, max_threads, audio_sample_rate);
}
}
}
@@ -0,0 +1,34 @@
/* 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 "global_managers.hpp"
namespace Granite
{
namespace Global
{
void init(ManagerFeatureFlags flags = MANAGER_FEATURE_DEFAULT_BITS,
unsigned max_threads = UINT_MAX, float audio_sample_rate = -1.0f);
}
}
@@ -0,0 +1,133 @@
/* 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 <functional>
#include <string>
#include "logging.hpp"
namespace Util
{
class MessageQueueInterface : public LoggingInterface
{
public:
virtual ~MessageQueueInterface() = default;
};
}
namespace Granite
{
class FilesystemInterface
{
public:
virtual ~FilesystemInterface() = default;
virtual bool load_text_file(const std::string &path, std::string &str) = 0;
};
class AssetManagerInterface
{
public:
virtual ~AssetManagerInterface() = default;
};
class MaterialManagerInterface
{
public:
virtual ~MaterialManagerInterface() = default;
virtual void iterate(AssetManagerInterface *iface) = 0;
};
class ThreadGroupInterface
{
public:
virtual ~ThreadGroupInterface() = default;
virtual void start(unsigned foreground_count, unsigned background_count,
const std::function<void()> &cb) = 0;
virtual void set_thread_context() = 0;
};
class EventManagerInterface
{
public:
virtual ~EventManagerInterface() = default;
};
class CommonRendererDataInterface
{
public:
virtual ~CommonRendererDataInterface() = default;
};
class PhysicsSystemInterface
{
public:
virtual ~PhysicsSystemInterface() = default;
};
class TouchDownEvent;
class TouchUpEvent;
class MouseMoveEvent;
class KeyboardEvent;
class OrientationEvent;
class TouchGestureEvent;
class MouseButtonEvent;
class JoypadButtonEvent;
class JoypadAxisEvent;
namespace UI
{
class UIManagerInterface
{
public:
virtual ~UIManagerInterface() = default;
virtual bool filter_input_event(const TouchDownEvent &e) = 0;
virtual bool filter_input_event(const TouchUpEvent &e) = 0;
virtual bool filter_input_event(const MouseMoveEvent &e) = 0;
virtual bool filter_input_event(const KeyboardEvent &e) = 0;
virtual bool filter_input_event(const OrientationEvent &e) = 0;
virtual bool filter_input_event(const TouchGestureEvent &e) = 0;
virtual bool filter_input_event(const MouseButtonEvent &e) = 0;
virtual bool filter_input_event(const JoypadButtonEvent &e) = 0;
virtual bool filter_input_event(const JoypadAxisEvent &e) = 0;
};
}
namespace Audio
{
class BackendInterface
{
public:
virtual ~BackendInterface() = default;
virtual bool start() = 0;
virtual bool stop() = 0;
};
class MixerInterface
{
public:
virtual ~MixerInterface() = default;
virtual void event_start(EventManagerInterface &event_manager) = 0;
virtual void event_stop(EventManagerInterface &event_manager) = 0;
};
}
}
@@ -0,0 +1,14 @@
add_granite_internal_lib(granite-input input.hpp input.cpp)
target_include_directories(granite-input PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-input PUBLIC granite-util granite-event granite-math)
if (${GRANITE_PLATFORM} MATCHES "SDL")
add_granite_internal_static_lib(granite-input-sdl input_sdl.cpp input_sdl.hpp)
target_link_libraries(granite-input-sdl PUBLIC granite-input)
if (GRANITE_SYSTEM_SDL)
find_package(SDL3 REQUIRED)
target_link_libraries(granite-input-sdl PUBLIC SDL3::SDL3-shared)
else()
target_link_libraries(granite-input-sdl PUBLIC SDL3-static)
endif()
endif()
@@ -0,0 +1,376 @@
/* 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 "input.hpp"
#include "event.hpp"
#include "muglm/muglm_impl.hpp"
#include "logging.hpp"
#include <algorithm>
#include <string.h>
using namespace Util;
namespace Granite
{
const char *joypad_key_to_tag(JoypadKey key)
{
#define D(k) case JoypadKey::k: return #k
switch (key)
{
D(Left);
D(Right);
D(Up);
D(Down);
D(LeftShoulder);
D(RightShoulder);
D(West);
D(East);
D(North);
D(South);
D(LeftThumb);
D(RightThumb);
D(Mode);
D(Start);
D(Select);
default:
return "Unknown";
}
#undef D
}
const char *joypad_axis_to_tag(JoypadAxis axis)
{
#define D(k) case JoypadAxis::k: return #k
switch (axis)
{
D(LeftX);
D(LeftY);
D(RightX);
D(RightY);
D(LeftTrigger);
D(RightTrigger);
default:
return "Unknown";
}
#undef D
}
void InputTracker::orientation_event(quat rot)
{
OrientationEvent event(rot);
if (handler)
handler->dispatch(event);
}
void InputTracker::on_touch_down(unsigned id, float x, float y)
{
if (touch.active_pointers >= TouchCount)
{
LOGE("Touch pointer overflow!\n");
return;
}
unsigned index = touch.active_pointers++;
auto &pointer = touch.pointers[index];
pointer.id = id;
pointer.start_x = x;
pointer.start_y = y;
pointer.last_x = x;
pointer.last_y = y;
pointer.x = x;
pointer.y = y;
TouchDownEvent event(index, id, x, y, touch.width, touch.height);
if (handler)
handler->dispatch(event);
}
void InputTracker::dispatch_touch_gesture()
{
TouchGestureEvent event(touch);
if (handler)
handler->dispatch(event);
}
void InputTracker::on_touch_move(unsigned id, float x, float y)
{
auto &pointers = touch.pointers;
auto itr = std::find_if(std::begin(pointers), std::begin(pointers) + touch.active_pointers, [id](const TouchState::Pointer &pointer) {
return pointer.id == id;
});
if (itr == std::end(pointers))
{
LOGE("Could not find pointer!\n");
return;
}
itr->x = x;
itr->y = y;
}
void InputTracker::on_touch_up(unsigned id, float x, float y)
{
auto &pointers = touch.pointers;
auto itr = std::find_if(std::begin(pointers), std::begin(pointers) + touch.active_pointers, [id](const TouchState::Pointer &pointer) {
return pointer.id == id;
});
if (itr == std::end(pointers))
{
LOGE("Could not find pointer!\n");
return;
}
auto index = itr - std::begin(pointers);
TouchUpEvent event(itr->id, x, y, itr->start_x, itr->start_y, touch.width, touch.height);
if (handler)
handler->dispatch(event);
memmove(&pointers[index], &pointers[index + 1], (TouchCount - (index + 1)) * sizeof(TouchState::Pointer));
touch.active_pointers--;
}
void InputTracker::joypad_key_state(unsigned index, JoypadKey key, JoypadKeyState state)
{
if (index >= Joypads)
return;
assert(active_joypads & (1u << index));
auto &joy = joypads[index];
unsigned key_index = Util::ecast(key);
unsigned key_mask = 1u << key_index;
if (state == JoypadKeyState::Pressed)
{
if ((joy.button_mask & key_mask) == 0)
{
JoypadButtonEvent event(index, key, state);
if (handler)
handler->dispatch(event);
}
joy.button_mask |= key_mask;
}
else if (state == JoypadKeyState::Released)
{
if ((joy.button_mask & key_mask) != 0)
{
JoypadButtonEvent event(index, key, state);
if (handler)
handler->dispatch(event);
}
joy.button_mask &= ~key_mask;
}
}
void JoypadState::snap_deadzone(float deadzone)
{
memcpy(snapped_axis, raw_axis, sizeof(raw_axis));
static const JoypadAxis fused_axes[2][2] = {
{ JoypadAxis::LeftX, JoypadAxis::LeftY },
{ JoypadAxis::RightX, JoypadAxis::RightY },
};
for (auto &fused : fused_axes)
{
if (std::abs(raw_axis[int(fused[0])]) < deadzone && std::abs(raw_axis[int(fused[1])]) < deadzone)
for (auto &axis : fused)
snapped_axis[int(axis)] = 0.0f;
}
}
void InputTracker::joyaxis_state(unsigned index, JoypadAxis axis, float value)
{
if (index >= Joypads)
return;
assert(active_joypads & (1u << index));
auto &joy = joypads[index];
unsigned axis_index = Util::ecast(axis);
auto &a = joy.raw_axis[axis_index];
if (a != value)
{
JoypadAxisEvent event(index, axis, value);
if (handler)
handler->dispatch(event);
}
a = value;
}
void InputTracker::key_event(Key key, KeyState state)
{
if (state == KeyState::Released)
key_state &= ~(1ull << ecast(key));
else if (state == KeyState::Pressed)
key_state |= 1ull << ecast(key);
KeyboardEvent event(key, state);
if (handler)
handler->dispatch(event);
}
void InputTracker::mouse_button_event(Granite::MouseButton button, bool pressed)
{
mouse_button_event(button, last_mouse_x, last_mouse_y, pressed);
}
void InputTracker::mouse_button_event(MouseButton button, double x, double y, bool pressed)
{
if (pressed)
mouse_button_state |= 1ull << ecast(button);
else
mouse_button_state &= ~(1ull << ecast(button));
if (mouse_active)
{
last_mouse_x = x;
last_mouse_y = y;
}
MouseButtonEvent event(button, x, y, pressed);
if (handler)
handler->dispatch(event);
}
void InputTracker::mouse_move_event_relative(double x, double y)
{
x *= mouse_speed_x;
y *= mouse_speed_y;
if (mouse_active)
{
last_mouse_x += x;
last_mouse_y += y;
last_mouse_x = clamp(last_mouse_x, mouse_relative_range_x,
mouse_relative_range_x + mouse_relative_range_width);
last_mouse_y = clamp(last_mouse_y, mouse_relative_range_y,
mouse_relative_range_y + mouse_relative_range_height);
MouseMoveEvent event(x, y, last_mouse_x, last_mouse_y, key_state, mouse_button_state);
if (handler)
handler->dispatch(event);
}
}
void InputTracker::mouse_move_event_absolute(double x, double y)
{
if (mouse_active)
{
double delta_x = x - last_mouse_x;
double delta_y = y - last_mouse_y;
last_mouse_x = x;
last_mouse_y = y;
MouseMoveEvent event(delta_x, delta_y, x, y, key_state, mouse_button_state);
if (handler)
handler->dispatch(event);
}
}
void InputTracker::mouse_move_event_absolute_normalized(double x, double y)
{
mouse_move_event_absolute(x * double(touch.width), y * double(touch.height));
}
void InputTracker::mouse_button_event_normalized(MouseButton button, double x, double y, bool pressed)
{
mouse_button_event(button, x * double(touch.width), y * double(touch.height), pressed);
}
void InputTracker::mouse_enter(double x, double y)
{
mouse_active = true;
last_mouse_x = x;
last_mouse_y = y;
}
void InputTracker::mouse_leave()
{
mouse_active = false;
}
void InputTracker::dispatch_current_state(double delta_time, InputTrackerHandler *override_handler)
{
if (!override_handler)
override_handler = handler;
if (override_handler)
{
for (auto &pad : joypads)
pad.snap_deadzone(axis_deadzone);
override_handler->dispatch(JoypadStateEvent{active_joypads, joypads, Joypads, delta_time});
override_handler->dispatch(InputStateEvent{last_mouse_x, last_mouse_y,
delta_time, key_state, mouse_button_state, mouse_active});
}
}
int InputTracker::find_vacant_joypad_index() const
{
for (int i = 0; i < Joypads; i++)
{
if ((active_joypads & (1 << i)) == 0)
return i;
}
return -1;
}
void InputTracker::enable_joypad(unsigned index, uint32_t vid, uint32_t pid)
{
if (index >= Joypads)
return;
if (active_joypads & (1u << index))
return;
active_joypads |= 1u << index;
joypads[index] = {};
joypads[index].vid = vid;
joypads[index].pid = pid;
JoypadConnectionEvent event(index, true, vid, pid);
if (handler)
handler->dispatch(event);
}
void InputTracker::disable_joypad(unsigned index, uint32_t vid, uint32_t pid)
{
if (index >= Joypads)
return;
if ((active_joypads & (1u << index)) == 0)
return;
active_joypads &= ~(1u << index);
joypads[index] = {};
JoypadConnectionEvent event(index, false, vid, pid);
if (handler)
handler->dispatch(event);
}
std::mutex &InputTracker::get_lock()
{
return dispatch_lock;
}
}
@@ -0,0 +1,719 @@
/* 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 "enum_cast.hpp"
#include "event.hpp"
#include <stdint.h>
#include "math.hpp"
#include <limits.h>
#include <float.h>
#include <mutex>
namespace Granite
{
enum class JoypadKey
{
Left,
Right,
Up,
Down,
East,
South,
West,
North,
LeftShoulder,
RightShoulder,
LeftThumb,
RightThumb,
Start,
Select,
Mode,
Count,
Unknown
};
const char *joypad_key_to_tag(JoypadKey key);
enum class JoypadAxis
{
LeftX,
LeftY,
RightX,
RightY,
LeftTrigger,
RightTrigger,
Count,
Unknown
};
const char *joypad_axis_to_tag(JoypadAxis axis);
enum class JoypadKeyState
{
Pressed,
Released,
Count
};
enum class Key
{
Unknown,
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
Return,
LeftCtrl,
LeftAlt,
LeftShift,
Space,
Escape,
Left, Right, Up, Down,
_1, _2, _3, _4, _5, _6, _7, _8, _9, _0,
Count
};
enum class MouseButton
{
Left,
Middle,
Right,
Count
};
enum class KeyState
{
Pressed,
Released,
Repeat,
Count
};
static_assert(Util::ecast(Key::Count) <= 64, "Cannot have more than 64 keys for bit-packing.");
struct TouchState
{
enum { PointerCount = 16 };
struct Pointer
{
unsigned id;
float start_x;
float start_y;
float last_x;
float last_y;
float x;
float y;
};
Pointer pointers[PointerCount] = {};
unsigned active_pointers = 0;
unsigned width;
unsigned height;
};
struct JoypadState
{
bool is_button_pressed(JoypadKey key) const
{
return (button_mask & (1u << Util::ecast(key))) != 0;
}
float get_axis(JoypadAxis a) const
{
return snapped_axis[Util::ecast(a)];
}
void snap_deadzone(float deadzone);
float raw_axis[Util::ecast(JoypadAxis::Count)] = {};
float snapped_axis[Util::ecast(JoypadAxis::Count)] = {};
uint32_t button_mask = 0;
uint32_t vid = 0;
uint32_t pid = 0;
};
static_assert(Util::ecast(JoypadKey::Count) <= 32, "Cannot have more than 32 joypad buttons.");
class InputTrackerHandler;
class InputTracker
{
public:
void key_event(Key key, KeyState state);
void mouse_button_event(MouseButton button, double x, double y, bool pressed);
void mouse_button_event_normalized(MouseButton button, double x, double y, bool pressed);
void mouse_button_event(MouseButton button, bool pressed);
void mouse_move_event_absolute(double x, double y);
void mouse_move_event_absolute_normalized(double x, double y);
void mouse_move_event_relative(double x, double y);
void dispatch_current_state(double delta_time, InputTrackerHandler *override_handler = nullptr);
void orientation_event(quat rot);
void joypad_key_state(unsigned index, JoypadKey key, JoypadKeyState state);
void joyaxis_state(unsigned index, JoypadAxis axis, float value);
void on_touch_down(unsigned id, float x, float y);
void on_touch_move(unsigned id, float x, float y);
void on_touch_up(unsigned id, float x, float y);
void mouse_enter(double x, double y);
void mouse_leave();
bool key_pressed(Key key) const
{
return (key_state & (1ull << Util::ecast(key))) != 0;
}
bool joykey_pressed(unsigned index, JoypadKey key) const
{
if (index >= Joypads)
return false;
return (joypads[index].button_mask & (1u << Util::ecast(key))) != 0;
}
bool mouse_button_pressed(MouseButton button) const
{
return (mouse_button_state & (1ull << Util::ecast(button))) != 0;
}
void dispatch_touch_gesture();
void set_axis_deadzone(float deadzone)
{
axis_deadzone = deadzone;
}
void set_relative_mouse_rect(double x, double y, double width, double height)
{
mouse_relative_range_x = x;
mouse_relative_range_y = y;
mouse_relative_range_width = width;
mouse_relative_range_height = height;
}
void set_relative_mouse_speed(double speed_x, double speed_y)
{
mouse_speed_x = speed_x;
mouse_speed_y = speed_y;
}
void enable_joypad(unsigned index, uint32_t vid, uint32_t pid);
void disable_joypad(unsigned index, uint32_t vid, uint32_t pid);
int find_vacant_joypad_index() const;
void set_touch_resolution(unsigned width, unsigned height)
{
touch.width = width;
touch.height = height;
}
void set_input_handler(InputTrackerHandler *handler_)
{
handler = handler_;
}
// To support dispatching input manager (i.e. polling) state from async threads.
std::mutex &get_lock();
enum { TouchCount = 16 };
enum { Joypads = 8 };
private:
InputTrackerHandler *handler = nullptr;
std::mutex dispatch_lock;
uint64_t key_state = 0;
uint8_t mouse_button_state = 0;
bool mouse_active = false;
double last_mouse_x = 0.0;
double last_mouse_y = 0.0;
double mouse_relative_range_x = 0.0;
double mouse_relative_range_y = 0.0;
double mouse_relative_range_width = DBL_MAX;
double mouse_relative_range_height = DBL_MAX;
double mouse_speed_x = 1.0;
double mouse_speed_y = 1.0;
uint8_t active_joypads = 0;
JoypadState joypads[Joypads] = {};
TouchState touch;
float axis_deadzone = 0.3f;
};
class JoypadConnectionEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadConnectionEvent)
JoypadConnectionEvent(unsigned index_, bool connected_, uint32_t vid_, uint32_t pid_)
: index(index_), connected(connected_), vid(vid_), pid(pid_)
{
}
unsigned get_index() const
{
return index;
}
bool is_connected() const
{
return connected;
}
uint32_t get_vid() const
{
return vid;
}
uint32_t get_pid() const
{
return pid;
}
private:
unsigned index;
bool connected;
uint32_t vid, pid;
};
class TouchGestureEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchGestureEvent)
explicit TouchGestureEvent(const TouchState &state_)
: state(state_)
{
}
const TouchState &get_state() const
{
return state;
}
private:
const TouchState &state;
};
class TouchDownEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchDownEvent)
TouchDownEvent(unsigned index_, unsigned id_,
float x_, float y_,
unsigned screen_width_, unsigned screen_height_)
: index(index_), id(id_),
x(x_), y(y_),
width(screen_width_), height(screen_height_)
{
}
float get_x() const
{
return x;
}
float get_y() const
{
return y;
}
unsigned get_index() const
{
return index;
}
unsigned get_id() const
{
return id;
}
unsigned get_screen_width() const
{
return width;
}
unsigned get_screen_height() const
{
return height;
}
private:
unsigned index, id;
float x, y;
unsigned width, height;
};
class TouchUpEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchUpEvent)
TouchUpEvent(unsigned id_, float x_, float y_,
float start_x_, float start_y_,
unsigned screen_width_, unsigned screen_height_)
: id(id_), x(x_), y(y_),
start_x(start_x_), start_y(start_y_),
width(screen_width_), height(screen_height_)
{
}
float get_x() const
{
return x;
}
float get_y() const
{
return y;
}
float get_start_x() const
{
return start_x;
}
float get_start_y() const
{
return start_y;
}
unsigned get_id() const
{
return id;
}
unsigned get_screen_width() const
{
return width;
}
unsigned get_screen_height() const
{
return height;
}
private:
unsigned id;
float x, y;
float start_x, start_y;
unsigned width, height;
};
class JoypadButtonEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadButtonEvent)
JoypadButtonEvent(unsigned index_, JoypadKey key_, JoypadKeyState state_)
: index(index_), key(key_), state(state_)
{
}
unsigned get_index() const
{
return index;
}
JoypadKey get_key() const
{
return key;
}
JoypadKeyState get_state() const
{
return state;
}
private:
unsigned index;
JoypadKey key;
JoypadKeyState state;
};
class JoypadAxisEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadAxisEvent)
JoypadAxisEvent(unsigned index_, JoypadAxis axis_, float value_)
: index(index_), axis(axis_), value(value_)
{
}
unsigned get_index() const
{
return index;
}
JoypadAxis get_axis() const
{
return axis;
}
float get_value() const
{
return value;
}
private:
unsigned index;
JoypadAxis axis;
float value;
};
class KeyboardEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(KeyboardEvent)
KeyboardEvent(Key key_, KeyState state_)
: key(key_), state(state_)
{
}
Key get_key() const
{
return key;
}
KeyState get_key_state() const
{
return state;
}
private:
Key key;
KeyState state;
};
class OrientationEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(OrientationEvent)
explicit OrientationEvent(const quat &rot_)
: rot(rot_)
{
}
const quat &get_rotation() const
{
return rot;
}
private:
quat rot;
};
class MouseButtonEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(MouseButtonEvent)
MouseButtonEvent(MouseButton button_, double abs_x_, double abs_y_, bool pressed_)
: button(button_), abs_x(abs_x_), abs_y(abs_y_), pressed(pressed_)
{
}
MouseButton get_button() const
{
return button;
}
double get_abs_x() const
{
return abs_x;
}
double get_abs_y() const
{
return abs_y;
}
bool get_pressed() const
{
return pressed;
}
private:
MouseButton button;
double abs_x;
double abs_y;
bool pressed;
};
class MouseMoveEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(MouseMoveEvent);
MouseMoveEvent(double delta_x_, double delta_y_, double abs_x_, double abs_y_,
uint64_t key_mask_, uint8_t btn_mask_)
: delta_x(delta_x_), delta_y(delta_y_),
abs_x(abs_x_), abs_y(abs_y_),
key_mask(key_mask_), btn_mask(btn_mask_)
{
}
bool get_mouse_button_pressed(MouseButton button) const
{
return (btn_mask & (1 << Util::ecast(button))) != 0;
}
bool get_key_pressed(Key key) const
{
return (key_mask & (1ull << Util::ecast(key))) != 0;
}
double get_delta_x() const
{
return delta_x;
}
double get_delta_y() const
{
return delta_y;
}
double get_abs_x() const
{
return abs_x;
}
double get_abs_y() const
{
return abs_y;
}
private:
double delta_x, delta_y, abs_x, abs_y;
uint64_t key_mask;
uint8_t btn_mask;
};
class JoypadStateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadStateEvent)
JoypadStateEvent(uint8_t active_mask_, const JoypadState *states_,
unsigned count_, double delta_time_)
: states(states_), count(count_), delta_time(delta_time_), active_mask(active_mask_)
{
}
bool is_connected(unsigned index) const
{
if (index >= count)
return false;
return (active_mask & (1u << index)) != 0;
}
unsigned get_num_indices() const
{
return count;
}
const JoypadState &get_state(unsigned index) const
{
return states[index];
}
double get_delta_time() const
{
return delta_time;
}
private:
const JoypadState *states;
unsigned count;
double delta_time;
uint8_t active_mask;
};
class InputStateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(InputStateEvent)
InputStateEvent(double abs_x_, double abs_y_,
double delta_time_, uint64_t key_mask_, uint8_t btn_mask_, bool mouse_active_)
: abs_x(abs_x_), abs_y(abs_y_),
delta_time(delta_time_), key_mask(key_mask_),
btn_mask(btn_mask_), mouse_active(mouse_active_)
{
}
double get_delta_time() const
{
return delta_time;
}
bool get_mouse_active() const
{
return mouse_active;
}
bool get_mouse_button_pressed(MouseButton button) const
{
return (btn_mask & (1 << Util::ecast(button))) != 0;
}
bool get_key_pressed(Key key) const
{
return (key_mask & (1ull << Util::ecast(key))) != 0;
}
double get_mouse_x() const
{
return abs_x;
}
double get_mouse_y() const
{
return abs_y;
}
private:
double abs_x, abs_y;
double delta_time;
uint64_t key_mask;
uint8_t btn_mask;
bool mouse_active;
};
class InputTrackerHandler
{
public:
virtual ~InputTrackerHandler() = default;
virtual void dispatch(const TouchDownEvent &e) = 0;
virtual void dispatch(const TouchUpEvent &e) = 0;
virtual void dispatch(const TouchGestureEvent &e) = 0;
virtual void dispatch(const JoypadButtonEvent &e) = 0;
virtual void dispatch(const JoypadAxisEvent &e) = 0;
virtual void dispatch(const KeyboardEvent &e) = 0;
virtual void dispatch(const OrientationEvent &e) = 0;
virtual void dispatch(const MouseButtonEvent &e) = 0;
virtual void dispatch(const MouseMoveEvent &e) = 0;
virtual void dispatch(const JoypadStateEvent &e) = 0;
virtual void dispatch(const InputStateEvent &e) = 0;
virtual void dispatch(const JoypadConnectionEvent &e) = 0;
};
}
@@ -0,0 +1,184 @@
/* 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 "input_sdl.hpp"
namespace Granite
{
bool InputTrackerSDL::init(InputTracker &tracker, const Dispatcher &dispatcher)
{
// Open existing gamepads.
int num_pads = 0;
SDL_JoystickID *gamepad_ids = SDL_GetGamepads(&num_pads);
for (int i = 0; i < num_pads; i++)
add_gamepad(gamepad_ids[i], tracker, dispatcher);
if (gamepad_ids)
SDL_free(gamepad_ids);
// Poll these separately, inline in poll_input().
SDL_SetGamepadEventsEnabled(false);
SDL_SetJoystickEventsEnabled(false);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_ADDED, true);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_REMOVED, true);
SDL_SetEventEnabled(SDL_EVENT_JOYSTICK_UPDATE_COMPLETE, false);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_UPDATE_COMPLETE, false);
return true;
}
void InputTrackerSDL::update(InputTracker &tracker)
{
SDL_UpdateGamepads();
for (int i = 0; i < int(InputTracker::Joypads); i++)
{
auto *pad = pads[i];
if (!pad)
continue;
static const struct
{
JoypadKey gkey;
SDL_GamepadButton sdl;
} buttons[] = {
{ JoypadKey::Left, SDL_GAMEPAD_BUTTON_DPAD_LEFT },
{ JoypadKey::Right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT },
{ JoypadKey::Up, SDL_GAMEPAD_BUTTON_DPAD_UP },
{ JoypadKey::Down, SDL_GAMEPAD_BUTTON_DPAD_DOWN },
{ JoypadKey::Start, SDL_GAMEPAD_BUTTON_START },
{ JoypadKey::Select, SDL_GAMEPAD_BUTTON_BACK },
{ JoypadKey::East, SDL_GAMEPAD_BUTTON_EAST },
{ JoypadKey::West, SDL_GAMEPAD_BUTTON_WEST },
{ JoypadKey::North, SDL_GAMEPAD_BUTTON_NORTH },
{ JoypadKey::South, SDL_GAMEPAD_BUTTON_SOUTH },
{ JoypadKey::LeftShoulder, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER },
{ JoypadKey::RightShoulder, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
{ JoypadKey::LeftThumb, SDL_GAMEPAD_BUTTON_LEFT_STICK },
{ JoypadKey::RightThumb, SDL_GAMEPAD_BUTTON_RIGHT_STICK },
{ JoypadKey::Mode, SDL_GAMEPAD_BUTTON_GUIDE },
};
for (auto &b : buttons)
{
tracker.joypad_key_state(i, b.gkey,
SDL_GetGamepadButton(pad, b.sdl) ?
JoypadKeyState::Pressed : JoypadKeyState::Released);
}
static const struct
{
JoypadAxis gaxis;
SDL_GamepadAxis sdl;
} axes[] = {
{ JoypadAxis::LeftX, SDL_GAMEPAD_AXIS_LEFTX },
{ JoypadAxis::LeftY, SDL_GAMEPAD_AXIS_LEFTY },
{ JoypadAxis::RightX, SDL_GAMEPAD_AXIS_RIGHTX },
{ JoypadAxis::RightY, SDL_GAMEPAD_AXIS_RIGHTY },
};
for (auto &a : axes)
{
float value = float(SDL_GetGamepadAxis(pad, a.sdl) - SDL_JOYSTICK_AXIS_MIN) /
float(SDL_JOYSTICK_AXIS_MAX - SDL_JOYSTICK_AXIS_MIN);
value = 2.0f * value - 1.0f;
tracker.joyaxis_state(i, a.gaxis, value);
}
tracker.joyaxis_state(i, JoypadAxis::LeftTrigger,
float(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) /
float(SDL_JOYSTICK_AXIS_MAX));
tracker.joyaxis_state(i, JoypadAxis::RightTrigger,
float(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) /
float(SDL_JOYSTICK_AXIS_MAX));
}
}
void InputTrackerSDL::close()
{
for (auto *pad : pads)
if (pad)
SDL_CloseGamepad(pad);
}
bool InputTrackerSDL::process_sdl_event(const SDL_Event &e, InputTracker &tracker,
const InputTrackerSDL::Dispatcher &dispatcher)
{
switch (e.type)
{
case SDL_EVENT_GAMEPAD_ADDED:
{
add_gamepad(e.gdevice.which, tracker, dispatcher);
return true;
}
case SDL_EVENT_GAMEPAD_REMOVED:
{
remove_gamepad(e.gdevice.which, tracker, dispatcher);
return true;
}
default:
break;
}
return false;
}
void InputTrackerSDL::add_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher)
{
int player_index = SDL_GetJoystickPlayerIndexForID(id);
if (player_index >= 0 && player_index < int(InputTracker::Joypads) && !pads[player_index])
{
uint32_t vid = SDL_GetGamepadVendorForID(id);
uint32_t pid = SDL_GetGamepadProductForID(id);
const char *name = SDL_GetGamepadNameForID(id);
LOGI("Plugging in controller: \"%s\" (%u/%u).\n", name, vid, pid);
const char *mapping = SDL_GetGamepadMappingForID(id);
LOGI(" Using mapping: \"%s\"\n", mapping);
pads[player_index] = SDL_OpenGamepad(id);
ids[player_index] = id;
dispatcher([player_index, vid, pid, &tracker]() {
tracker.enable_joypad(player_index, vid, pid);
});
}
}
void InputTrackerSDL::remove_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher)
{
for (int i = 0; i < int(InputTracker::Joypads); i++)
{
if (pads[i] && ids[i] == id)
{
uint32_t vid = SDL_GetGamepadVendor(pads[i]);
uint32_t pid = SDL_GetGamepadProduct(pads[i]);
SDL_CloseGamepad(pads[i]);
pads[i] = nullptr;
ids[i] = 0;
dispatcher([i, vid, pid, &tracker]() {
tracker.disable_joypad(i, vid, pid);
});
break;
}
}
}
}
@@ -0,0 +1,45 @@
/* 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 <SDL3/SDL.h>
#include "input.hpp"
#include <functional>
namespace Granite
{
class InputTrackerSDL
{
public:
using Dispatcher = std::function<void (std::function<void ()>)>;
bool init(InputTracker &tracker, const Dispatcher &dispatcher);
void close();
bool process_sdl_event(const SDL_Event &event, InputTracker &tracker, const Dispatcher &dispatcher);
void update(InputTracker &tracker);
private:
SDL_Gamepad *pads[InputTracker::Joypads] = {};
SDL_JoystickID ids[InputTracker::Joypads] = {};
void add_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher);
void remove_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher);
};
}
@@ -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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,180 @@
/* 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 "application.hpp"
#include "render_context.hpp"
#include "scene_loader.hpp"
#include "animation_system.hpp"
#include "renderer.hpp"
#include "timer.hpp"
#include "event.hpp"
#include "font.hpp"
#include "ui_manager.hpp"
#include "render_graph.hpp"
#include "mesh_util.hpp"
#include "scene_renderer.hpp"
#include "lights/clusterer.hpp"
#include "lights/volumetric_fog.hpp"
#include "lights/volumetric_diffuse.hpp"
#include "camera_export.hpp"
#include "post/aa.hpp"
#include "post/temporal.hpp"
namespace Granite
{
class TaskComposer;
class SceneViewerApplication : public Application, public EventHandler
{
public:
struct CLIConfig
{
bool timestamp = false;
bool ocean = false;
int camera_index = -1;
};
SceneViewerApplication(const std::string &path,
const std::string &config_path, const std::string &quirks_path,
const CLIConfig &cli_config);
~SceneViewerApplication();
void render_frame(double frame_time, double elapsed_time) override;
void rescale_scene(float radius);
void loop_animations();
protected:
void update_scene(TaskComposer &composer, double frame_time, double elapsed_time);
void render_scene(TaskComposer &composer);
void post_frame() override;
RenderContext context;
RenderContext depth_contexts[NumShadowCascades];
RenderContext fallback_depth_context;
RendererSuite renderer_suite;
RendererSuite::Config renderer_suite_config;
FlatRenderer flat_renderer;
LightingParameters lighting;
LightingParameters fallback_lighting;
FPSCamera cam;
SceneLoader scene_loader;
std::unique_ptr<AnimationSystem> animation_system;
Camera *selected_camera = nullptr;
DirectionalLightComponent *selected_directional = nullptr;
DirectionalLightComponent default_directional_light;
SceneTransformManager scene_transform_manager;
void on_device_created(const Vulkan::DeviceCreatedEvent &e);
void on_device_destroyed(const Vulkan::DeviceCreatedEvent &e);
void on_swapchain_changed(const Vulkan::SwapchainParameterEvent &e);
void on_swapchain_destroyed(const Vulkan::SwapchainParameterEvent &e);
bool on_key_down(const KeyboardEvent &e);
RenderGraph graph;
bool need_shadow_map_update = true;
AABB shadow_scene_aabb;
std::unique_ptr<LightClusterer> cluster;
std::unique_ptr<VolumetricFog> volumetric_fog;
std::unique_ptr<VolumetricDiffuseLightManager> volumetric_diffuse;
RenderQueue queue;
void setup_shadow_map();
void update_shadow_scene_aabb();
void render_ui(Vulkan::CommandBuffer &cmd);
void add_main_pass(Vulkan::Device &device, const std::string &tag);
void add_main_pass_forward(Vulkan::Device &device, const std::string &tag);
void add_main_pass_deferred(Vulkan::Device &device, const std::string &tag);
void add_mv_pass(const std::string &tag, const std::string &depth, bool full_mv);
void add_shadow_pass(Vulkan::Device &device, const std::string &tag);
void add_shadow_pass_fallback(Vulkan::Device &device, const std::string &tag);
std::vector<RecordedCamera> recorded_cameras;
std::string get_name() override;
void bake_render_graph(const Vulkan::SwapchainParameterEvent &swap);
const Vulkan::SwapchainParameterEvent *pending_swapchain = nullptr;
private:
void read_config(const std::string &path);
void read_quirks(const std::string &path);
void read_lights();
struct Config
{
RendererType renderer_type = RendererType::GeneralDeferred;
unsigned msaa = 1;
float shadow_map_resolution = 2048.0f;
unsigned clustered_lights_shadow_resolution = 512;
SceneRendererFlags pcf_flags = SCENE_RENDERER_SHADOW_PCF_WIDE_BIT;
float resolution_scale = 1.0f;
bool resolution_scale_sharpen = true;
float lod_bias = 0.0f;
bool directional_light_shadows = true;
bool directional_light_cascaded_shadows = true;
bool directional_light_shadows_vsm = false;
bool clustered_lights_shadows = true;
bool clustered_lights_shadows_vsm = false;
bool hdr_bloom = true;
bool hdr_bloom_dynamic_exposure = true;
bool forward_depth_prepass = true;
bool rt_fp16 = false;
bool rescale_scene = false;
bool show_ui = true;
bool volumetric_fog = false;
bool volumetric_fog_regions = true;
bool volumetric_diffuse = false;
bool ssao = true;
bool debug_probes = false;
bool ssr = false;
PostAAType postaa_type = PostAAType::None;
};
Config config;
CLIConfig cli_config;
void export_lights();
void export_cameras();
enum { FrameWindowSize = 64, FrameWindowSizeMask = FrameWindowSize - 1 };
float last_frame_times[FrameWindowSize] = {};
unsigned last_frame_index = 0;
TemporalJitter jitter;
void capture_environment_probe();
RenderTextureResource *ssao_output = nullptr;
RenderTextureResource *shadows = nullptr;
RenderTextureResource *fallback_shadows = nullptr;
RenderTextureResource *hiz_main = nullptr;
RenderTextureResource *hiz_depth = nullptr;
Util::SmallVector<Vulkan::ImageViewHandle> hiz_depth_peel;
CullingPassesInfo culling_passes_info = {};
};
}