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