diff --git a/Cargo.lock b/Cargo.lock
index d7577747..59310619 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3190,6 +3190,14 @@ dependencies = [
"winresource",
]
+[[package]]
+name = "pyrowave-sys"
+version = "0.11.0"
+dependencies = [
+ "bindgen",
+ "cmake",
+]
+
[[package]]
name = "quick-error"
version = "1.2.3"
diff --git a/Cargo.toml b/Cargo.toml
index bbea5015..7ba9018a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ members = [
"crates/pf-console-ui",
"crates/pf-ffvk",
"crates/pf-driver-proto",
+ "crates/pyrowave-sys",
"clients/probe",
"clients/linux",
"clients/session",
diff --git a/crates/pyrowave-sys/CMakeLists.txt b/crates/pyrowave-sys/CMakeLists.txt
new file mode 100644
index 00000000..769f1895
--- /dev/null
+++ b/crates/pyrowave-sys/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.20)
+project(pyrowave-sys LANGUAGES CXX C)
+
+if(NOT DEFINED PYROWAVE_VENDOR_DIR)
+ set(PYROWAVE_VENDOR_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/pyrowave)
+endif()
+
+# Mirror upstream pyrowave's standalone (top-level) Granite configuration
+# (CMakeLists.txt lines 88-99 at the vendored pin), but produce static
+# archives only — the Rust crate links the C API statically.
+set(GRANITE_RENDERER OFF CACHE BOOL "" FORCE)
+set(GRANITE_VULKAN_SPIRV_CROSS OFF CACHE BOOL "" FORCE)
+set(GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER OFF CACHE BOOL "" FORCE)
+set(GRANITE_VULKAN_SYSTEM_HANDLES OFF CACHE BOOL "" FORCE)
+set(GRANITE_SHADER_COMPILER_OPTIMIZE OFF CACHE BOOL "" FORCE)
+set(GRANITE_POSITION_INDEPENDENT ON CACHE BOOL "" FORCE)
+set(GRANITE_VULKAN_FOSSILIZE OFF CACHE BOOL "" FORCE)
+set(GRANITE_SHIPPING ON CACHE BOOL "" FORCE)
+set(GRANITE_PLATFORM "null" CACHE STRING "" FORCE)
+
+add_subdirectory(${PYROWAVE_VENDOR_DIR}/Granite ${CMAKE_CURRENT_BINARY_DIR}/Granite EXCLUDE_FROM_ALL)
+# Not top-level from here, so upstream's CMake only defines the static
+# `pyrowave` codec library (shared lib / tests / install rules are skipped).
+add_subdirectory(${PYROWAVE_VENDOR_DIR} ${CMAKE_CURRENT_BINARY_DIR}/pyrowave EXCLUDE_FROM_ALL)
+
+# The C API as a static archive. Upstream only ships it as a SHARED target;
+# compiling pyrowave_c.cpp against the same deps is equivalent minus the
+# dllexport attributes (PYROWAVE_EXPORT_SYMBOLS stays undefined).
+add_library(pyrowave-capi STATIC ${PYROWAVE_VENDOR_DIR}/pyrowave_c.cpp)
+target_link_libraries(pyrowave-capi PRIVATE pyrowave granite-vulkan)
diff --git a/crates/pyrowave-sys/Cargo.toml b/crates/pyrowave-sys/Cargo.toml
new file mode 100644
index 00000000..3c0723dd
--- /dev/null
+++ b/crates/pyrowave-sys/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "pyrowave-sys"
+description = "Vendored PyroWave (Themaister's intra-only wavelet codec, Vulkan compute) built from source + bindgen over its C API — the LAN low-latency codec backend (design/pyrowave-codec-plan.md). Upstream pin recorded in vendor/pyrowave/PUNKTFUNK-VENDOR.txt; bumping it is a protocol-affecting change (§4.2)."
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+authors.workspace = true
+repository.workspace = true
+links = "pyrowave"
+
+[build-dependencies]
+# Same CMake-from-vendored-source model as opus/audiopus_sys: reproducible
+# offline builds (CI, MSVC, flatpak — the flatpak builder has no network).
+cmake = "0.1"
+# Same bindgen configuration as pf-ffvk (runtime = dlopen libclang).
+bindgen = { version = "0.72", features = ["runtime"], default-features = false }
diff --git a/crates/pyrowave-sys/build.rs b/crates/pyrowave-sys/build.rs
new file mode 100644
index 00000000..c2454b20
--- /dev/null
+++ b/crates/pyrowave-sys/build.rs
@@ -0,0 +1,103 @@
+//! Build the vendored PyroWave codec (C++/Vulkan compute) as static archives via
+//! CMake and generate bindings over its C API (`pyrowave.h`).
+//!
+//! Linux + Windows only — the platforms whose hosts/clients run the Vulkan codec
+//! path (design/pyrowave-codec-plan.md §5). Other targets get an empty bindings
+//! file so the workspace builds everywhere (the Apple client is a native Metal
+//! port, §4.7 — it never links this crate).
+//!
+//! Everything compiles from the committed vendor tree: no network, no system
+//! pyrowave, no pkg-config — CI, MSVC, and the offline flatpak builder all get
+//! reproducible builds (§4.1). Vulkan headers come vendored too; only a libclang
+//! for bindgen is required on the build machine.
+
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+ println!("cargo:rerun-if-changed=wrapper.h");
+ println!("cargo:rerun-if-changed=CMakeLists.txt");
+ println!("cargo:rerun-if-changed=vendor/pyrowave");
+
+ let out = PathBuf::from(env::var("OUT_DIR").unwrap());
+ let bindings_path = out.join("bindings.rs");
+
+ let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
+ if target_os != "linux" && target_os != "windows" {
+ std::fs::write(
+ &bindings_path,
+ "// pyrowave-sys: Linux/Windows-only, empty on this target\n",
+ )
+ .unwrap();
+ return;
+ }
+
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+ let vendor = manifest_dir.join("vendor/pyrowave");
+ let vk_include = vendor.join("Granite/third_party/khronos/vulkan-headers/include");
+
+ // Always Release: a debug build of the wavelet kernels' host code buys no
+ // debuggability (the hot path is GPU shaders baked into the source) and the
+ // MSVC debug CRT would clash with Rust's release CRT.
+ let dst = cmake::Config::new(&manifest_dir)
+ .profile("Release")
+ .build_target("pyrowave-capi")
+ .build();
+ let build = dst.join("build");
+
+ // Static link closure, dependents before dependencies (GNU ld is order-sensitive).
+ println!("cargo:rustc-link-search=native={}", build.display());
+ for sub in [
+ "pyrowave",
+ "Granite/vulkan",
+ "Granite/util",
+ "Granite/math",
+ "Granite/third_party",
+ ] {
+ println!(
+ "cargo:rustc-link-search=native={}",
+ build.join(sub).display()
+ );
+ // MSVC multi-config generators put archives in a Release/ subdir.
+ println!(
+ "cargo:rustc-link-search=native={}",
+ build.join(sub).join("Release").display()
+ );
+ }
+ println!(
+ "cargo:rustc-link-search=native={}",
+ build.join("Release").display()
+ );
+ for lib in [
+ "pyrowave-capi",
+ "pyrowave",
+ "granite-vulkan",
+ "granite-util",
+ "granite-math",
+ "granite-volk",
+ ] {
+ println!("cargo:rustc-link-lib=static={lib}");
+ }
+ if target_os == "linux" {
+ println!("cargo:rustc-link-lib=dylib=stdc++");
+ // volk loads the Vulkan loader at runtime.
+ println!("cargo:rustc-link-lib=dylib=dl");
+ println!("cargo:rustc-link-lib=dylib=pthread");
+ }
+
+ let bindings = bindgen::Builder::default()
+ .header("wrapper.h")
+ .clang_arg(format!("-I{}", vendor.display()))
+ .clang_arg(format!("-I{}", vk_include.display()))
+ .allowlist_function("pyrowave_.*")
+ .allowlist_type("pyrowave_.*")
+ .allowlist_var("PYROWAVE_.*")
+ // The Vk* handles/structs the API mentions come along via the allowlist
+ // closure; they are ABI-identical to ash's, callers cast at the seam.
+ .derive_default(true)
+ .generate()
+ .expect("bindgen failed for pyrowave.h");
+ bindings
+ .write_to_file(&bindings_path)
+ .expect("failed to write pyrowave bindings");
+}
diff --git a/crates/pyrowave-sys/src/lib.rs b/crates/pyrowave-sys/src/lib.rs
new file mode 100644
index 00000000..9b91a0e8
--- /dev/null
+++ b/crates/pyrowave-sys/src/lib.rs
@@ -0,0 +1,29 @@
+//! Raw FFI bindings to the vendored PyroWave C API (`pyrowave.h`).
+//!
+//! Empty on targets other than Linux/Windows — see build.rs. The safe wrapper
+//! lives with its consumer (`punktfunk-host`'s encoder backend, and later the
+//! Rust clients' decoder backend); this crate is bindings only.
+
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// Bindgen output for a C API: u128 layout warnings and the like are upstream's concern.
+#![allow(improper_ctypes)]
+
+#[cfg(any(target_os = "linux", target_os = "windows"))]
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+
+#[cfg(all(test, any(target_os = "linux", target_os = "windows")))]
+mod tests {
+ use super::*;
+
+ /// Link sanity: statically linked archives resolve and the vendored pin is
+ /// the API version we designed against. No GPU or Vulkan loader required —
+ /// the version query touches no device state.
+ #[test]
+ fn api_version_matches_vendored_pin() {
+ let (mut major, mut minor, mut patch) = (0u32, 0u32, 0u32);
+ unsafe { pyrowave_get_api_version(&mut major, &mut minor, &mut patch) };
+ assert_eq!((major, minor, patch), (0, 4, 0), "vendored pyrowave API version moved — re-check the §4.2 protocol coupling before bumping");
+ }
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/CMakeLists.txt b/crates/pyrowave-sys/vendor/pyrowave/CMakeLists.txt
new file mode 100644
index 00000000..490f62a5
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/CMakeLists.txt
@@ -0,0 +1,173 @@
+cmake_minimum_required(VERSION 3.20)
+set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_C_STANDARD 99)
+project(pyrowave LANGUAGES CXX C)
+
+if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
+ set(PYROWAVE_CXX_FLAGS -Wshadow -Wall -Wextra -Wno-comment -Wno-missing-field-initializers -Wno-empty-body -fvisibility=hidden)
+ if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang")
+ set(PYROWAVE_CXX_FLAGS ${PYROWAVE_CXX_FLAGS} -Wno-backslash-newline-escape)
+ endif()
+ if (NOT (${CMAKE_BUILD_TYPE} MATCHES "Release"))
+ message("Enabling frame pointer for profiling/debug.")
+ set(PYROWAVE_CXX_FLAGS ${PYROWAVE_CXX_FLAGS} -fno-omit-frame-pointer)
+ endif()
+ if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)")
+ message("Enabling SSE3 support.")
+ set(PYROWAVE_CXX_FLAGS ${PYROWAVE_CXX_FLAGS} -msse3)
+ endif()
+elseif (MSVC)
+ set(PYROWAVE_CXX_FLAGS /D_CRT_SECURE_NO_WARNINGS /wd4267 /wd4244 /wd4309 /wd4005 /MP)
+endif()
+
+add_library(pyrowave STATIC
+ pyrowave_config.hpp shaders/slangmosh.hpp
+ pyrowave_encoder.hpp pyrowave_encoder.cpp
+ pyrowave_decoder.hpp pyrowave_decoder.cpp
+ pyrowave_common.hpp pyrowave_common.cpp)
+
+target_include_directories(pyrowave PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_compile_options(pyrowave PRIVATE ${PYROWAVE_CXX_FLAGS})
+set_target_properties(pyrowave PROPERTIES POSITION_INDEPENDENT_CODE ON)
+
+option(PYROWAVE_DEVEL "Build PyroWave as a standalone sandbox with development tooling." OFF)
+
+if (ANDROID)
+ # Mobile chips really like FP16
+ target_compile_definitions(pyrowave PRIVATE PYROWAVE_PRECISION=0)
+ message("Forcing FP16 default on Android.")
+else()
+ option(PYROWAVE_FP32_STORAGE "Configure for full FP32 and extended precision." OFF)
+ option(PYROWAVE_FP32_MATH "Configure for FP32 arithmetic, but reduced range storage." ON)
+ if (PYROWAVE_FP32_STORAGE)
+ target_compile_definitions(pyrowave PRIVATE PYROWAVE_PRECISION=2)
+ elseif(PYROWAVE_FP32_MATH)
+ target_compile_definitions(pyrowave PRIVATE PYROWAVE_PRECISION=1)
+ else()
+ target_compile_definitions(pyrowave PRIVATE PYROWAVE_PRECISION=0)
+ endif()
+endif()
+
+if (PYROWAVE_DEVEL)
+ add_library(pyrowave-utils STATIC yuv4mpeg.cpp yuv4mpeg.hpp)
+
+ # Standalone build, build the sandbox.
+ set(GRANITE_RENDERER ON CACHE BOOL "" FORCE)
+ set(GRANITE_VULKAN_FOSSILIZE OFF CACHE BOOL "" FORCE)
+ set(GRANITE_SHIPPING OFF CACHE BOOL "" FORCE)
+ set(GRANITE_PLATFORM "SDL" CACHE STRING "" FORCE)
+ set(GRANITE_HIDDEN ON CACHE BOOL "" FORCE)
+ set(GRANITE_POSITION_INDEPENDENT ON CACHE BOOL "" FORCE)
+ add_subdirectory(Granite EXCLUDE_FROM_ALL)
+
+ add_granite_offline_tool(pyrowave-sandbox sandbox.cpp)
+ target_link_libraries(pyrowave-sandbox PRIVATE pyrowave)
+ target_compile_definitions(pyrowave-sandbox PRIVATE ASSET_DIRECTORY=\"${CMAKE_CURRENT_SOURCE_DIR}/shaders\")
+ target_link_libraries(pyrowave-sandbox PRIVATE granite-renderer pyrowave-utils)
+
+ add_granite_offline_tool(pyrowave-bench bench.cpp)
+ target_link_libraries(pyrowave-bench PRIVATE pyrowave)
+ target_link_libraries(pyrowave-bench PRIVATE granite-vulkan pyrowave-utils)
+
+ add_granite_offline_tool(pyrowave-encode encode.cpp)
+ target_link_libraries(pyrowave-encode PRIVATE pyrowave)
+ target_link_libraries(pyrowave-encode PRIVATE granite-vulkan pyrowave-utils)
+
+ add_granite_offline_tool(pyrowave-decode decode.cpp)
+ target_link_libraries(pyrowave-decode PRIVATE pyrowave)
+ target_link_libraries(pyrowave-decode PRIVATE granite-vulkan pyrowave-utils)
+
+ add_granite_offline_tool(pyrowave-psnr psnr.cpp)
+ target_link_libraries(pyrowave-psnr PRIVATE pyrowave-utils)
+
+ add_granite_application(pyrowave-viewer viewer.cpp)
+ target_link_libraries(pyrowave-viewer PRIVATE pyrowave pyrowave-utils)
+ if (NOT ANDROID)
+ target_compile_definitions(pyrowave-viewer PRIVATE ASSET_DIRECTORY=\"${CMAKE_CURRENT_SOURCE_DIR}/shaders\")
+ endif()
+elseif (${PROJECT_IS_TOP_LEVEL} AND (NOT TARGET granite-vulkan))
+ set(GRANITE_RENDERER OFF CACHE BOOL "" FORCE)
+ set(GRANITE_VULKAN_SPIRV_CROSS OFF CACHE BOOL "" FORCE)
+ set(GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER OFF CACHE BOOL "" FORCE)
+ set(GRANITE_VULKAN_SYSTEM_HANDLES OFF CACHE BOOL "" FORCE)
+ set(GRANITE_SHADER_COMPILER_OPTIMIZE OFF CACHE BOOL "" FORCE)
+ set(GRANITE_POSITION_INDEPENDENT ON CACHE BOOL "" FORCE)
+ set(GRANITE_VULKAN_FOSSILIZE OFF CACHE BOOL "" FORCE)
+ set(GRANITE_SHIPPING ON CACHE BOOL "" FORCE)
+ set(GRANITE_PLATFORM "null" CACHE STRING "" FORCE)
+ add_subdirectory(Granite EXCLUDE_FROM_ALL)
+endif()
+
+target_link_libraries(pyrowave PRIVATE granite-vulkan granite-math)
+
+if (${PROJECT_IS_TOP_LEVEL})
+ add_library(pyrowave-shared SHARED pyrowave_c.cpp pyrowave.h)
+ target_link_libraries(pyrowave-shared PRIVATE pyrowave granite-vulkan)
+ target_compile_definitions(pyrowave-shared PRIVATE PYROWAVE_EXPORT_SYMBOLS)
+ target_compile_options(pyrowave-shared PRIVATE ${PYROWAVE_CXX_FLAGS})
+
+ add_executable(pyrowave-c-test pyrowave_c_test.cpp)
+ target_link_libraries(pyrowave-c-test PRIVATE pyrowave-shared granite-volk-headers)
+ target_compile_options(pyrowave-c-test PRIVATE ${PYROWAVE_CXX_FLAGS})
+
+ add_executable(pyrowave-c-interop-test pyrowave_c_interop_test.cpp com_ptr.hpp)
+ target_link_libraries(pyrowave-c-interop-test PRIVATE pyrowave-shared granite-vulkan)
+ target_compile_options(pyrowave-c-interop-test PRIVATE ${PYROWAVE_CXX_FLAGS})
+
+ add_executable(pyrowave-device-validation pyrowave_device_validation.cpp com_ptr.hpp)
+ target_link_libraries(pyrowave-device-validation PRIVATE pyrowave-shared granite-util granite-volk-headers)
+ target_compile_options(pyrowave-device-validation PRIVATE ${PYROWAVE_CXX_FLAGS})
+
+ set(PYROWAVE_API_VERSION_MAJOR 0)
+ set(PYROWAVE_API_VERSION_MINOR 4)
+ set(PYROWAVE_API_VERSION_PATCH 0)
+ set(PYROWAVE_API_VERSION ${PYROWAVE_API_VERSION_MAJOR}.${PYROWAVE_API_VERSION_MINOR}.${PYROWAVE_API_VERSION_PATCH})
+
+ # DLL_NAME_WITH_SOVERSION requires CMake 3.27+. SteamRT doesn't have CMake 3.27 yet though.
+ if (WIN32 AND (CMAKE_VERSION VERSION_LESS "3.27"))
+ message(FATAL_ERROR "CMake build on Windows expects version 3.27 at minimum.")
+ endif()
+
+ set_target_properties(pyrowave-shared
+ PROPERTIES VERSION ${PYROWAVE_API_VERSION}
+ SOVERSION ${PYROWAVE_API_VERSION_MAJOR}
+ DLL_NAME_WITH_SOVERSION ON)
+
+ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/pyrowave.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pyrowave)
+ install(TARGETS pyrowave-shared EXPORT pyrowave-sharedConfig
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pyrowave)
+ install(EXPORT pyrowave-sharedConfig DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pyrowave-shared/cmake)
+ install(TARGETS pyrowave-device-validation RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+ set(PYROWAVE_INSTALL_LIB_DIR ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR})
+ set(PYROWAVE_INSTALL_INC_DIR ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/pyrowave)
+ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/pkg-config/pyrowave-shared.pc.in
+ ${CMAKE_CURRENT_BINARY_DIR}/pyrowave-shared.pc @ONLY)
+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pyrowave-shared.pc DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig)
+
+ if (WIN32 AND CMAKE_COMPILER_IS_GNUCXX)
+ target_link_libraries(pyrowave-shared PRIVATE -static gcc stdc++ winpthread)
+ target_link_libraries(pyrowave-c-test PRIVATE -static gcc stdc++ winpthread)
+ target_link_libraries(pyrowave-c-interop-test PRIVATE -static gcc stdc++ winpthread)
+ target_link_libraries(pyrowave-device-validation PRIVATE -static gcc stdc++ winpthread)
+ endif()
+
+ if (NOT WIN32)
+ target_link_libraries(pyrowave-shared PRIVATE -pthread)
+ target_link_libraries(pyrowave-c-test PRIVATE -pthread)
+ target_link_libraries(pyrowave-c-interop-test PRIVATE -pthread)
+ target_link_libraries(pyrowave-device-validation PRIVATE -pthread)
+ endif()
+
+ if (WIN32)
+ target_sources(pyrowave-shared PRIVATE pyrowave-shared.def)
+ set_target_properties(pyrowave-shared PROPERTIES PREFIX "lib")
+ target_link_libraries(pyrowave-c-interop-test PRIVATE d3d11 d3d12 dxgi)
+ else()
+ target_link_options(pyrowave-shared PRIVATE -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/link.T)
+ endif()
+endif()
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/.clang-format b/crates/pyrowave-sys/vendor/pyrowave/Granite/.clang-format
new file mode 100755
index 00000000..443f90b7
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/.clang-format
@@ -0,0 +1,167 @@
+# The style used for all options not specifically set in the configuration.
+BasedOnStyle: LLVM
+
+# The extra indent or outdent of access modifiers, e.g. public:.
+AccessModifierOffset: -4
+
+# If true, aligns escaped newlines as far left as possible. Otherwise puts them into the right-most column.
+AlignEscapedNewlinesLeft: true
+
+# If true, aligns trailing comments.
+AlignTrailingComments: false
+
+# Allow putting all parameters of a function declaration onto the next line even if BinPackParameters is false.
+AllowAllParametersOfDeclarationOnNextLine: false
+
+# Allows contracting simple braced statements to a single line.
+AllowShortBlocksOnASingleLine: false
+
+# If true, short case labels will be contracted to a single line.
+AllowShortCaseLabelsOnASingleLine: false
+
+# Dependent on the value, int f() { return 0; } can be put on a single line. Possible values: None, Inline, All.
+AllowShortFunctionsOnASingleLine: None
+
+# If true, if (a) return; can be put on a single line.
+AllowShortIfStatementsOnASingleLine: false
+
+# If true, while (true) continue; can be put on a single line.
+AllowShortLoopsOnASingleLine: false
+
+# If true, always break after function definition return types.
+AlwaysBreakAfterDefinitionReturnType: false
+
+# If true, always break before multiline string literals.
+AlwaysBreakBeforeMultilineStrings: false
+
+# If true, always break after the template<...> of a template declaration.
+AlwaysBreakTemplateDeclarations: true
+
+# If false, a function call's arguments will either be all on the same line or will have one line each.
+BinPackArguments: true
+
+# If false, a function declaration's or function definition's parameters will either all be on the same line
+# or will have one line each.
+BinPackParameters: true
+
+# The way to wrap binary operators. Possible values: None, NonAssignment, All.
+BreakBeforeBinaryOperators: None
+
+# The brace breaking style to use. Possible values: Attach, Linux, Stroustrup, Allman, GNU.
+BreakBeforeBraces: Allman
+
+# If true, ternary operators will be placed after line breaks.
+BreakBeforeTernaryOperators: false
+
+# Always break constructor initializers before commas and align the commas with the colon.
+BreakConstructorInitializersBeforeComma: true
+
+# The column limit. A column limit of 0 means that there is no column limit.
+ColumnLimit: 120
+
+# A regular expression that describes comments with special meaning, which should not be split into lines or otherwise changed.
+CommentPragmas: '^ *'
+
+# If the constructor initializers don't fit on a line, put each initializer on its own line.
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+
+# The number of characters to use for indentation of constructor initializer lists.
+ConstructorInitializerIndentWidth: 4
+
+# Indent width for line continuations.
+ContinuationIndentWidth: 4
+
+# If true, format braced lists as best suited for C++11 braced lists.
+Cpp11BracedListStyle: false
+
+# Disables formatting at all.
+DisableFormat: false
+
+# A vector of macros that should be interpreted as foreach loops instead of as function calls.
+#ForEachMacros: ''
+
+# Indent case labels one level from the switch statement.
+# When false, use the same indentation level as for the switch statement.
+# Switch statement body is always indented one level more than case labels.
+IndentCaseLabels: false
+
+# The number of columns to use for indentation.
+IndentWidth: 4
+
+# Indent if a function definition or declaration is wrapped after the type.
+IndentWrappedFunctionNames: false
+
+# If true, empty lines at the start of blocks are kept.
+KeepEmptyLinesAtTheStartOfBlocks: true
+
+# Language, this format style is targeted at. Possible values: None, Cpp, Java, JavaScript, Proto.
+Language: Cpp
+
+# The maximum number of consecutive empty lines to keep.
+MaxEmptyLinesToKeep: 1
+
+# The indentation used for namespaces. Possible values: None, Inner, All.
+NamespaceIndentation: None
+
+# The penalty for breaking a function call after "call(".
+PenaltyBreakBeforeFirstCallParameter: 19
+
+# The penalty for each line break introduced inside a comment.
+PenaltyBreakComment: 300
+
+# The penalty for breaking before the first <<.
+PenaltyBreakFirstLessLess: 120
+
+# The penalty for each line break introduced inside a string literal.
+PenaltyBreakString: 1000
+
+# The penalty for each character outside of the column limit.
+PenaltyExcessCharacter: 1000000
+
+# Penalty for putting the return type of a function onto its own line.
+PenaltyReturnTypeOnItsOwnLine: 1000000000
+
+# Pointer and reference alignment style. Possible values: Left, Right, Middle.
+PointerAlignment: Right
+
+# If true, a space may be inserted after C style casts.
+SpaceAfterCStyleCast: false
+
+# If false, spaces will be removed before assignment operators.
+SpaceBeforeAssignmentOperators: true
+
+# Defines in which cases to put a space before opening parentheses. Possible values: Never, ControlStatements, Always.
+SpaceBeforeParens: ControlStatements
+
+# If true, spaces may be inserted into '()'.
+SpaceInEmptyParentheses: false
+
+# The number of spaces before trailing line comments (// - comments).
+SpacesBeforeTrailingComments: 1
+
+# If true, spaces will be inserted after '<' and before '>' in template argument lists.
+SpacesInAngles: false
+
+# If true, spaces may be inserted into C style casts.
+SpacesInCStyleCastParentheses: false
+
+# If true, spaces are inserted inside container literals (e.g. ObjC and Javascript array and dict literals).
+SpacesInContainerLiterals: false
+
+# If true, spaces will be inserted after '(' and before ')'.
+SpacesInParentheses: false
+
+# If true, spaces will be inserted after '[' and befor']'.
+SpacesInSquareBrackets: false
+
+# Format compatible with this standard, e.g. use A > instead of A> for LS_Cpp03. Possible values: Cpp03, Cpp11, Auto.
+Standard: Cpp11
+
+# The number of columns used for tab stops.
+TabWidth: 4
+
+# The way to use tab characters in the resulting file. Possible values: Never, ForIndentation, Always.
+UseTab: ForIndentation
+
+# Do not reflow comments
+ReflowComments: false
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/.gitmodules b/crates/pyrowave-sys/vendor/pyrowave/Granite/.gitmodules
new file mode 100644
index 00000000..fc829003
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/.gitmodules
@@ -0,0 +1,54 @@
+[submodule "third_party/spirv-cross"]
+ path = third_party/spirv-cross
+ url = https://github.com/KhronosGroup/SPIRV-Cross
+[submodule "third_party/shaderc"]
+ path = third_party/shaderc
+ url = https://github.com/google/shaderc.git
+[submodule "third_party/glslang"]
+ path = third_party/glslang
+ url = https://github.com/Themaister/glslang.git
+[submodule "third_party/spirv-tools"]
+ path = third_party/spirv-tools
+ url = https://github.com/Themaister/SPIRV-Tools
+[submodule "third_party/spirv-headers"]
+ path = third_party/spirv-headers
+ url = https://github.com/KhronosGroup/SPIRV-Headers
+[submodule "third_party/rapidjson"]
+ path = third_party/rapidjson
+ url = https://github.com/miloyip/rapidjson
+[submodule "third_party/stb/stb"]
+ path = third_party/stb/stb
+ url = https://github.com/nothings/stb
+[submodule "third_party/astc-encoder"]
+ path = third_party/astc-encoder
+ url = https://github.com/ARM-software/astc-encoder
+[submodule "third_party/fossilize"]
+ path = third_party/fossilize
+ url = https://github.com/ValveSoftware/Fossilize
+[submodule "third_party/volk"]
+ path = third_party/volk
+ url = https://github.com/zeux/volk
+[submodule "third_party/meshoptimizer"]
+ path = third_party/meshoptimizer
+ url = https://github.com/zeux/meshoptimizer
+[submodule "third_party/muFFT"]
+ path = third_party/muFFT
+ url = https://github.com/Themaister/muFFT
+[submodule "third_party/oboe"]
+ path = third_party/oboe
+ url = https://github.com/google/oboe
+[submodule "third_party/khronos/vulkan-headers"]
+ path = third_party/khronos/vulkan-headers
+ url = https://github.com/KhronosGroup/Vulkan-Headers
+[submodule "third_party/fsr2"]
+ path = third_party/fsr2
+ url = https://github.com/Themaister/FidelityFX-FSR2
+[submodule "third_party/sdl3"]
+ path = third_party/sdl3
+ url = https://github.com/libsdl-org/SDL
+[submodule "third_party/pyroenc"]
+ path = third_party/pyroenc
+ url = https://github.com/HansKristian-Work/pyroenc
+[submodule "third_party/pyrowave"]
+ path = third_party/pyrowave
+ url = https://github.com/Themaister/pyrowave
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/CMakeLists.txt b/crates/pyrowave-sys/vendor/pyrowave/Granite/CMakeLists.txt
new file mode 100644
index 00000000..8d5adb0d
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/CMakeLists.txt
@@ -0,0 +1,386 @@
+cmake_minimum_required(VERSION 3.6)
+set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_C_STANDARD 99)
+project(Granite LANGUAGES CXX C)
+
+if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
+ set(GRANITE_CXX_FLAGS -Wshadow -Wall -Wextra -Wno-comment -Wno-missing-field-initializers -Wno-empty-body)
+ if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang")
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -Wno-backslash-newline-escape)
+ endif()
+ if (NOT (${CMAKE_BUILD_TYPE} MATCHES "Release"))
+ message("Enabling frame pointer for profiling/debug.")
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -fno-omit-frame-pointer)
+ endif()
+ if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)")
+ message("Enabling SSE3 support.")
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -msse3)
+ endif()
+elseif (MSVC)
+ set(GRANITE_CXX_FLAGS /D_CRT_SECURE_NO_WARNINGS /wd4267 /wd4244 /wd4309 /wd4005 /MP)
+endif()
+
+include(GNUInstallDirs)
+
+option(GRANITE_SHARED "Build Granite as a shared library." OFF)
+option(GRANITE_ISPC_TEXTURE_COMPRESSION "Enable ISPC texture compression" OFF)
+option(GRANITE_ASTC_ENCODER_COMPRESSION "Enable astc-encoder texture compression" OFF)
+option(GRANITE_TOOLS "Build Granite tools." ON)
+option(GRANITE_KHR_DISPLAY_ACQUIRE_XLIB "Try to acquire Xlib display when using VK_KHR_display." OFF)
+option(GRANITE_ANDROID_APK_FILESYSTEM "Use APK file system for assets and builtin files." ON)
+option(GRANITE_ANDROID_SWAPPY "Use swappy on Android." OFF)
+option(GRANITE_SHADER_COMPILER_OPTIMIZE "Optimize SPIR-V." ON)
+option(GRANITE_VULKAN_SYSTEM_HANDLES "Add system handle support to Vulkan backend." ON)
+option(GRANITE_RENDERER "Add higher level rendering functionality." ON)
+option(GRANITE_VULKAN_SPIRV_CROSS "Add reflection support to Vulkan backend." ON)
+option(GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER "Enable Vulkan GLSL runtime compiler." ON)
+option(GRANITE_VULKAN_FOSSILIZE "Enable support for Fossilize." ON)
+option(GRANITE_VULKAN_PROFILES "Enable profiles support." OFF)
+option(GRANITE_VULKAN_DXGI_INTEROP "Enable DXGI interop when running on Windows." ON)
+option(GRANITE_VULKAN_POST_MORTEM "Add extra support for port-mortem debug." OFF)
+option(GRANITE_AUDIO "Enable Audio support." OFF)
+option(GRANITE_PLATFORM "Granite Platform" "SDL")
+option(GRANITE_HIDDEN "Declare symbols as hidden by default. Useful if you build Granite as a static library and you link to it in your shared library." OFF)
+option(GRANITE_SANITIZE_ADDRESS "Sanitize address" OFF)
+option(GRANITE_SANITIZE_THREADS "Sanitize threads" OFF)
+option(GRANITE_SANITIZE_MEMORY "Sanitize memory" OFF)
+option(GRANITE_TARGET_NATIVE "Target native arch (-march=native)" OFF)
+option(GRANITE_BULLET "Enable Bullet support." OFF)
+option(GRANITE_RENDERDOC_CAPTURE "Enable support for RenderDoc capture from API." ON)
+option(GRANITE_INSTALL_TARGETS "Add install targets." ON)
+option(GRANITE_INSTALL_EXE_TARGETS "Add executable install targets." OFF)
+option(GRANITE_FFMPEG "Enable FFmpeg." OFF)
+option(GRANITE_FFMPEG_VULKAN "Enable experimental Vulkan HW decode support in FFmpeg." OFF)
+option(GRANITE_FAST_MATH "Enable fast math." OFF)
+option(GRANITE_SHIPPING "Disable code paths not related to development." OFF)
+option(GRANITE_SYSTEM_SDL "Use system SDL3 instead of vendored submodule." OFF)
+
+if (GRANITE_FAST_MATH)
+ message("Enabling fast math.")
+ if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -ffast-math)
+ elseif (MSVC)
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} /fp:fast)
+ endif()
+else()
+ message("Enabling precise math.")
+ if (MSVC)
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} /fp:precise)
+ endif()
+endif()
+
+if (NOT GRANITE_PLATFORM)
+ set(GRANITE_PLATFORM "SDL")
+endif()
+
+if (GRANITE_HIDDEN)
+ if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
+ endif()
+elseif(${GRANITE_PLATFORM} MATCHES "libretro")
+ if (NOT GRANITE_SHARED)
+ if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
+ endif()
+ endif()
+endif()
+
+if (GRANITE_TARGET_NATIVE)
+ if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native")
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=native")
+ endif()
+endif()
+
+set(GRANITE_LINK_FLAGS)
+
+if (${CMAKE_VERSION} GREATER "3.13")
+ # Executable targets are not stripped in release mode on Android, do it explicitly.
+ add_link_options($<$,$>:-s>)
+endif()
+
+if (GRANITE_SANITIZE_ADDRESS)
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -fsanitize=address)
+ set(GRANITE_LINK_FLAGS "${GRANITE_LINK_FLAGS} -fsanitize=address")
+endif()
+
+if (GRANITE_SANITIZE_THREADS)
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -fsanitize=thread)
+ set(GRANITE_LINK_FLAGS "${GRANITE_LINK_FLAGS} -fsanitize=thread")
+endif()
+
+if (GRANITE_SANITIZE_MEMORY)
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} -fsanitize=memory)
+ set(GRANITE_LINK_FLAGS "${GRANITE_LINK_FLAGS} -fsanitize=memory")
+endif()
+
+if (ANDROID)
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+endif()
+
+if (GRANITE_SHARED)
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+endif()
+
+if (GRANITE_SHARED)
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+else()
+ if (${GRANITE_PLATFORM} MATCHES "libretro")
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+ elseif (GRANITE_POSITION_INDEPENDENT)
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+ elseif (ANDROID)
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+ else()
+ set(CMAKE_POSITION_INDEPENDENT_CODE OFF)
+ endif()
+endif()
+
+set(GRANITE_VERSION_MAJOR 0)
+set(GRANITE_VERSION_MINOR 0)
+set(GRANITE_VERSION_PATCH 0)
+
+# Make sure .dlls are placed next to .exe, since there is no RPATH.
+if (WIN32)
+ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
+endif()
+
+function(granite_install_executable NAME)
+ install(TARGETS ${NAME} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+ if (NOT WIN32)
+ set_target_properties(${NAME} PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib")
+ endif()
+endfunction()
+
+macro(granite_setup_default_link_libraries NAME)
+ if (NOT WIN32)
+ target_link_libraries(${NAME} PRIVATE -pthread dl m)
+ find_library(LIBRT_LIBRARY rt)
+ if (EXISTS ${LIBRT_LIBRARY})
+ target_link_libraries(${NAME} PUBLIC rt)
+ endif()
+ endif()
+ if (WIN32 AND CMAKE_COMPILER_IS_GNUCXX)
+ target_link_libraries(${NAME} PRIVATE -static gcc stdc++ winpthread)
+ endif()
+endmacro()
+
+function(add_granite_third_party_lib NAME)
+ if (GRANITE_SHARED)
+ add_library(${NAME} SHARED ${ARGN})
+
+ if (GRANITE_INSTALL_TARGETS)
+ if (WIN32)
+ install(TARGETS ${NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
+ else()
+ install(TARGETS ${NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR})
+ set_target_properties(${NAME} PROPERTIES INSTALL_RPATH "\$ORIGIN/")
+ set_target_properties(${NAME} PROPERTIES
+ VERSION ${GRANITE_VERSION_MAJOR}.${GRANITE_VERSION_MINOR}.${GRANITE_VERSION_PATCH}
+ SOVERSION ${GRANITE_VERSION_MAJOR})
+ endif()
+ endif()
+
+ granite_setup_default_link_libraries(${NAME})
+
+ if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
+ set_target_properties(${NAME} PROPERTIES LINK_FLAGS "-Wl,--no-undefined")
+ endif()
+ if (MSVC)
+ set_target_properties(${NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
+ endif()
+ else()
+ add_library(${NAME} STATIC ${ARGN})
+ if (MSVC)
+ set_target_properties(${NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS OFF)
+ endif()
+ endif()
+endfunction()
+
+function(add_granite_internal_lib NAME)
+ add_granite_third_party_lib(${NAME} ${ARGN})
+ target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
+ if (GRANITE_SHARED AND MSVC)
+ set_target_properties(${NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
+ endif()
+endfunction()
+
+function(add_granite_internal_static_lib NAME)
+ add_library(${NAME} STATIC ${ARGN})
+ target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
+endfunction()
+
+add_subdirectory(third_party)
+add_subdirectory(util)
+add_subdirectory(path)
+add_subdirectory(math)
+add_subdirectory(threading)
+add_subdirectory(compiler)
+add_subdirectory(filesystem)
+add_subdirectory(vulkan)
+add_subdirectory(ecs)
+add_subdirectory(event)
+if (GRANITE_VULKAN_SYSTEM_HANDLES AND GRANITE_RENDERER)
+ add_subdirectory(renderer)
+ add_subdirectory(ui)
+ add_subdirectory(scene-export)
+endif()
+
+if (GRANITE_FFMPEG)
+ add_subdirectory(video)
+endif()
+
+add_subdirectory(application)
+
+if (GRANITE_AUDIO)
+ add_subdirectory(audio)
+endif()
+if (GRANITE_BULLET)
+ add_subdirectory(physics)
+endif()
+
+add_library(granite-base INTERFACE)
+target_link_libraries(granite-base INTERFACE granite-util granite-path granite-application granite-vulkan)
+if (TARGET granite-renderer)
+ target_link_libraries(granite-base INTERFACE granite-renderer granite-ui)
+endif()
+
+function(add_granite_executable NAME)
+ if (ANDROID)
+ add_library(${NAME} SHARED ${ARGN})
+ target_link_libraries(${NAME} PRIVATE log android)
+ elseif(${GRANITE_PLATFORM} MATCHES "libretro")
+ add_library(${NAME} SHARED ${ARGN})
+ if (CMAKE_COMPILER_IS_GNUCXX OR (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang"))
+ set_target_properties(${NAME} PROPERTIES LINK_FLAGS "-Wl,--no-undefined")
+ endif()
+ else()
+ add_executable(${NAME} ${ARGN})
+ endif()
+ # Need this for some reason on OSX.
+ if (APPLE)
+ set_target_properties(${NAME} PROPERTIES LINK_FLAGS "-Wl,-all_load")
+ endif()
+ target_link_libraries(${NAME} PRIVATE granite-base granite-application-global-init)
+ target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
+ granite_setup_default_link_libraries(${NAME})
+ if (GRANITE_SANITIZE_ADDRESS OR GRANITE_SANITIZE_THREADS OR GRANITE_SANITIZE_MEMORY)
+ set_target_properties(${NAME} PROPERTIES LINK_FLAGS "${GRANITE_LINK_FLAGS}")
+ endif()
+endfunction()
+
+function(add_granite_application TARGET_NAME)
+ if (WIN32 AND (NOT ${GRANITE_PLATFORM} MATCHES "libretro"))
+ add_granite_executable(${TARGET_NAME} WIN32 ${ARGN})
+ else()
+ add_granite_executable(${TARGET_NAME} ${ARGN})
+ endif()
+ if (ANDROID)
+ target_link_libraries(${TARGET_NAME} PRIVATE granite-base granite-platform)
+ elseif (${GRANITE_PLATFORM} MATCHES "libretro")
+ target_link_libraries(${TARGET_NAME} PRIVATE granite-base granite-platform)
+ else()
+ target_link_libraries(${TARGET_NAME} PRIVATE granite-application-entry)
+ endif()
+ granite_setup_default_link_libraries(${TARGET_NAME})
+ if (GRANITE_INSTALL_EXE_TARGETS)
+ granite_install_executable(${TARGET_NAME})
+ endif()
+endfunction()
+
+function(add_granite_headless_application TARGET_NAME)
+ add_executable(${TARGET_NAME} ${ARGN})
+
+ target_compile_options(${TARGET_NAME} PRIVATE ${GRANITE_CXX_FLAGS})
+ if (GRANITE_SANITIZE_ADDRESS OR GRANITE_SANITIZE_THREADS OR GRANITE_SANITIZE_MEMORY)
+ set_target_properties(${TARGET_NAME} PROPERTIES LINK_FLAGS "${GRANITE_LINK_FLAGS}")
+ endif()
+ if (ANDROID)
+ target_link_libraries(${TARGET_NAME} PRIVATE log android)
+ endif()
+ target_link_libraries(${TARGET_NAME} PRIVATE granite-base granite-application-entry-headless)
+ granite_setup_default_link_libraries(${TARGET_NAME})
+ if (GRANITE_INSTALL_EXE_TARGETS)
+ granite_install_executable(${TARGET_NAME})
+ endif()
+endfunction()
+
+function(add_granite_offline_tool NAME)
+ add_executable(${NAME} ${ARGN})
+ target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
+
+ # Need this for some reason on OSX.
+ if (APPLE)
+ set_target_properties(${NAME} PROPERTIES LINK_FLAGS "-Wl,-all_load")
+ endif()
+ target_link_libraries(${NAME} PRIVATE granite-base granite-application-global-init)
+ target_compile_options(${NAME} PRIVATE ${GRANITE_CXX_FLAGS})
+ if (GRANITE_SANITIZE_ADDRESS OR GRANITE_SANITIZE_THREADS OR GRANITE_SANITIZE_MEMORY)
+ set_target_properties(${NAME} PROPERTIES LINK_FLAGS "${GRANITE_LINK_FLAGS}")
+ endif()
+ granite_setup_default_link_libraries(${NAME})
+ if (GRANITE_INSTALL_EXE_TARGETS)
+ granite_install_executable(${NAME})
+ endif()
+endfunction()
+
+if ("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_SOURCE_DIR}")
+ set(GRANITE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+else()
+ set(GRANITE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" PARENT_SCOPE)
+ set(GRANITE_CXX_FLAGS ${GRANITE_CXX_FLAGS} PARENT_SCOPE)
+ set(GRANITE_LINK_FLAGS ${GRANITE_LINK_FLAGS} PARENT_SCOPE)
+endif()
+
+function(add_blob_archive_target TARGET_NAME BLOB_NAME)
+ find_package(Python3 REQUIRED)
+ set_source_files_properties(
+ ${CMAKE_BINARY_DIR}/${BLOB_NAME}.h
+ ${CMAKE_BINARY_DIR}/${BLOB_NAME}.c
+ PROPERTIES GENERATED ON)
+ add_library(${TARGET_NAME} STATIC
+ ${CMAKE_BINARY_DIR}/${BLOB_NAME}.h
+ ${CMAKE_BINARY_DIR}/${BLOB_NAME}.c)
+ target_include_directories(${TARGET_NAME} PUBLIC ${CMAKE_BINARY_DIR})
+ # ... CMake, y u gotta be like this.
+ set(_arg_list)
+ set(_arg_counter 0)
+ foreach(_arg IN LISTS ARGN)
+ math(EXPR _arg_counter_mod "${_arg_counter} % 2")
+ if (NOT ${_arg_counter_mod})
+ list(APPEND _arg_list --input)
+ endif()
+ list(APPEND _arg_list ${_arg})
+ math(EXPR _arg_counter "${_arg_counter} + 1")
+ endforeach()
+ add_custom_command(OUTPUT
+ ${CMAKE_BINARY_DIR}/${BLOB_NAME}.h
+ ${CMAKE_BINARY_DIR}/${BLOB_NAME}.c
+ COMMAND ${Python3_EXECUTABLE}
+ ${GRANITE_SOURCE_DIR}/tools/blobify.py
+ --output ${CMAKE_BINARY_DIR}/${BLOB_NAME}.blob
+ ${_arg_list}
+ COMMAND ${Python3_EXECUTABLE}
+ ${GRANITE_SOURCE_DIR}/tools/bin_to_text.py
+ --output ${CMAKE_BINARY_DIR}/${BLOB_NAME}
+ --blob-name ${BLOB_NAME}
+ ${CMAKE_BINARY_DIR}/${BLOB_NAME}.blob
+ BYPRODUCTS
+ ${CMAKE_BINARY_DIR}/${BLOB_NAME}.blob)
+endfunction()
+
+if (TARGET granite-renderer)
+ add_subdirectory(tests)
+ add_subdirectory(viewer)
+ if (GRANITE_TOOLS)
+ add_subdirectory(tools)
+ add_subdirectory(renderer/fft/test)
+ endif()
+endif()
+
+if (GRANITE_TOOLS AND GRANITE_VULKAN_SPIRV_CROSS AND GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER)
+ add_subdirectory(slangmosh)
+endif()
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/LICENSE b/crates/pyrowave-sys/vendor/pyrowave/Granite/LICENSE
new file mode 100644
index 00000000..e13a3380
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/LICENSE
@@ -0,0 +1,20 @@
+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.
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/OVERVIEW.md b/crates/pyrowave-sys/vendor/pyrowave/Granite/OVERVIEW.md
new file mode 100644
index 00000000..afd82779
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/OVERVIEW.md
@@ -0,0 +1,632 @@
+# Granite code base overview
+
+The top level structure contains several folders which contain high level concepts.
+
+## Build system
+
+The build system is pure CMake and should be very straight forward to use.
+To build Granite as a standalone project for example to run the glTF viewer, it's standard CMake:
+
+```
+mkdir build
+cd build
+cmake .. -DCMAKE_BUILD_TYPE=Release
+make -j16 # Or whatever build system cmake spits out.
+./viewer/gltf-viewer /path/to/my.gltf
+```
+
+A Python installation is probably necessary for SPIRV-Tools to build.
+
+### Submodules
+
+Granite uses submodules to pull in third party modules. Make sure that
+`git submodule update --init` is called when checking out new versions of Granite.
+
+### Building Android apps with Granite
+
+There is no builtin way of building Android apps, but it's fairly straight forward to set up.
+You can use the `gltf-viewer` as an example.
+
+#### Setting up the Gradle app folder
+
+You'll need an app folder. This is found under `viewer/app`, but you will need to make your own.
+Here you place the Android manifest. The manifest needs to point to the GraniteActivity.
+This activity is basically just NativeActivity with a few extra utility functions.
+The lib_name needs to point to a particular native library which is the
+CMake target you have chosen to use for your CMake `add_granite_application()`.
+See `viewer/CMakeLists.txt` for how to use `add_granite_application()`.
+
+See `viewer/app/AndroidManifest.xml` for an example manifest file.
+`viewer/app/build.gradle` connects CMake, the manifest, where to pull assets from, etc.
+Make sure that the builtin resources in assets/ are pulled in as well as your own assets.
+The viewer application has its own asset folder in `viewer/assets`.
+For example, to try the Android version of the `gltf-viewer`, place a scene called `scene.glb`
+into `viewer/assets` and build. By default, the Android app will load from `assets://scene.glb`, which will
+point to the APKs asset manager. You can place `android.json`, `config.json` and `quirks.json` into the assets folder as well.
+See the code for more detail.
+
+In `viewer/app/res`, various icons and string resources should go as normal.
+The default `AndroidManifest.xml` points to a built-in Android icon, so you'll probably have to add that.
+
+In `viewer/app/build.gradle` replace what is needed. Likely, only the `targets` line.
+Otherwise, it can mostly be copy-pasted.
+`viewer/build.gradle` is weird magic that has to be there.
+The gradle plugin version might depend a bit on your Android Studio installation.
+`viewer/settings.gradle` pulls in your app as well as the simple, common GraniteActivity Java cruft.
+
+Once you've set it up, a normal gradle build should suffice.
+
+### Using Granite as a dependency
+
+Granite is designed to be used as a statically linked library, but can be built with position independent code if you need
+to wrap your application in a dynamic library (e.g. libretro target).
+
+The normal idea is to have Granite somewhere as a folder in your tree, either a submodule, symlink or whatever.
+Create a CMake project and add Granite as a subdirectory.
+Use `add_granite_application()` to set up the build.
+It's mostly just a convenience script to link in relevant targets.
+
+```
+cmake_minimum_required(VERSION 3.5)
+set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_C_STANDARD 99)
+project(AppBringup LANGUAGES CXX C)
+
+add_subdirectory(Granite EXCLUDE_FROM_ALL)
+
+add_granite_application(app-bringup app_bringup.cpp)
+target_link_libraries(app-bringup renderer)
+if (NOT ANDROID)
+ target_compile_definitions(app-bringup PRIVATE ASSET_DIRECTORY=\"${CMAKE_CURRENT_SOURCE_DIR}/assets\")
+endif()
+```
+
+### Using Granite as a pure Vulkan backend
+
+Some projects would only be interested in the raw Vulkan backend.
+The Vulkan backend has some dependencies on other Granite modules which might end up unwieldy.
+Most of these can be stripped out, e.g.:
+
+```
+# If using Granite as part of your shared library.
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+set(GRANITE_POSITION_INDEPENDENT ON CACHE STRING "Granite position independent" FORCE)
+set(GRANITE_HIDDEN ON CACHE STRING "Granite hidden" FORCE)
+
+# Disable dependencies on other modules.
+set(GRANITE_VULKAN_FOSSILIZE OFF CACHE STRING "Vulkan Fossilize" FORCE)
+set(GRANITE_VULKAN_SYSTEM_HANDLES OFF CACHE STRING "Vulkan filesystem" FORCE)
+add_subdirectory(Granite EXCLUDE_FROM_ALL)
+```
+
+At this point, it should only depend on SPIRV-Cross and volk.
+
+## `application/`
+
+This module implements anything related to an applications lifecycle.
+Platform specific application things go in here as well, like various backends for `VkSurfaceKHR`,
+and input handling.
+
+- Android
+- GLFW
+- Headless (no WSI, used for benchmarking)
+- VK_KHR_display
+- libretro
+- Custom plugin surface (used if you have some fancy, special purpose surface code).
+
+This module defines the "Application" interface, used by the platform code in the main loop.
+The platform code implements main() or whatever equivalent the platform requires.
+
+The gltf-viewer application is also implemented here by `SceneViewerApplication`.
+This application can load glTF scenes, and you can move around, add lights, I use it as a sandbox for testing stuff.
+
+See `application.cpp` for how to implement a `Granite::Application` interface.
+You basically implement a `render_frame()` callback.
+
+Applications are expected to implement `Granite::application_create()`, which should create
+an instance of the application, and the platform code will pump this through.
+Applications do not own their main loop as some platforms do not support that.
+
+It is perfectly possible to avoid this and just use the Vulkan backend API directly.
+This is useful for standalone tooling which needs to use the GPU.
+See `tools/convert_cube_to_environment.cpp` as an example here.
+
+## `assets/`
+
+Here the builtin assets for Granite are found. Various shaders for the most part and a couple of look-up textures.
+These are not relevant if you only use Granite for its Vulkan backend.
+
+## `compiler/`
+
+Here `third_party/shaderc` is used to implement GLSL -> SPIR-V compilation in run-time,
+with or without optimizations with SPIRV-Tools-opt.
+I do this for convenience sake.
+A production application will probably want to pre-compile shaders and ship the SPIR-V only.
+
+## `event/`
+
+Here you find the event manager. The event manager is global, and is used to dispatch events throughout the application in a decoupled way.
+There are two kinds of events, latched and immediate. Latched events are events which continue to persist until they are destroyed.
+This is used for things like "device created" and "swapchain created". The device remains alive until it dies, similar with the swapchain.
+For new event handler registrations, the event manager will make sure to fire events for device created events even if the device created event has already been fired.
+This allows subsystems to always know when the Vulkan device is alive, or dead without having to plumb a ton of state through all the subsystems.
+
+Immediate events are things like "key pressed", "mouse moved" etc. These events fire once and disappear.
+Event handlers must be member function which inherit from `Granite::EventHandler`.
+You can easily create your own events by tagging a type.
+
+E.g.:
+
+```
+class SwapchainParameterEvent : public Granite::Event
+{
+public:
+ GRANITE_EVENT_TYPE_DECL(SwapchainParameterEvent)
+};
+
+class TestApp : public Granite::EventHandler
+{
+ TestApp()
+ {
+ EVENT_MANAGER_REGISTER_LATCH(TestApp, on_swapchain_created, on_swapchain_destroyed, SwapchainParameterEvent);
+ EVENT_MANAGER_REGISTER(TestApp, on_key_pressed, KeyboardEvent);
+ }
+
+ // return true to keep responding to these events, false to detach yourself.
+ bool on_key_pressed(const KeyboardEvent &e);
+
+ void on_swapchain_created(const SwapchainParameterEvent &e);
+ void on_swapchain_destroyed(const SwapchainParameterEvent &);
+};
+```
+
+This module also implements a simple Entity Component System (ECS). (Not sure why I moved it to event/, but anyways).
+This is used by the scene graph for various purposes. Some pretty funky variadic template magic happens here.
+You create entities, and components are added to them. You can query for component groups, e.g. "give me all renderables which should be rendered opaque", things like that.
+It's a very neat system for different kind of queries.
+It's likely very unoptimized compared to what it could be,
+but I haven't had any issues except that removing components is quite costly.
+
+## `filesystem/`
+
+This implements a filesystem.
+The design here is a bit radical. The only file operations supported are `mmap()` or `MapViewOfFile()`. No stream API exists.
+On Android, the APK buffer mapping feature is used (which should basically map to mmap anyways for uncompressed files).
+
+The path system is based around protocols where a path is `protocol://path/inside/protocol`. The builtin protocols are:
+- `builtin://`. Should always refer to `assets/` in some shape or form, where you can find the Granite builtin shaders and so on.
+- `assets://`. Application specific assets. Usually set up by the application manually.
+- `cache://`. Used for various caching purposes internally. Usually just points to `build_directory/cache`.
+
+Paths without a protocol are thought to be of the `file://` protocol, which is basically the same as just using the normal filesystem
+path system directly.
+
+The filesystem supports notions of notifications when anything changes. There is a backend for inotify on Linux.
+This is used to automatically reload textures and shaders when they are modified on disk. Very useful for hacking.
+
+The protocols are set up automatically on startup, but can be overridden by the application.
+
+## `math/`
+
+A simple math library cloning the parts of GLM I need. It used to be GLM, but it got bogged down in very long compile times,
+so I rewrote only what I needed and sped up compilation by ~40/50% o_O.
+
+Matrix math conventions are like GLM:
+- Matrices are column major. Indexing into a matrix picks out columns.
+- Quaternions are declared in order W, X, Y, Z.
+- The coordinate system is GL-style view space, a right handed coordinate system. In view space +X is right, +Y is up, camera points towards -Z.
+- Clip space is Vulkan-like (duh). X is right, Y is down (top-left, not bottom-left like GL!), Z = 1 is far plane, Z = 0 is near plane.
+ Basically the projection matrices are like GL, except Y is flipped. I could have gone for bottom-left clip space,
+ but I would require VK_KHR_maintenance1 which isn't supported on some old Android phones (yay ...).
+
+## `network/`
+
+Some Linux-only networking code lying around.
+It is used by the networked filesystem code, but it hasn't been worked on for a long time.
+
+## `renderer/`
+
+The biggest module. This module is responsible for implementing the high level rendering
+code which deals with the problem of meshes, materials, scene graphs, lights, etc, etc.
+
+### `renderer/fft/`
+
+A Vulkan port of my GLFFT library. Used by Ocean rendering.
+
+### `renderer/lights/`
+
+Light rendering, including a clustered shading system.
+
+### `renderer/post/`
+
+Post-FX. HDR bloom and tonemapping, FXAA, SMAA and TAA.
+
+### `renderer/utils/`
+
+Some simple utility functions, like converting cubes and equirect into IBL and so on.
+It can also read back and save textures (GTX format) to disk, even for arrays, cubes, etc.
+
+### `abstract_renderable.hpp`
+
+Renderable objects inherit from this. The `Renderer` will ask the `AbstractRenderable` for rendering info, which is pushed into a `RenderQueue`.
+See `mesh.cpp` for the `StaticMesh` implementation.
+
+### `render_components.hpp`
+
+Some component types used in the renderer, like `OpaqueComponent`, `RenderableComponent`, etc.
+Applications will pull out the components which are relevant and act on all of entities which have
+certain combinations of components.
+
+### The flow of rendering objects
+
+You should study `SceneViewerApplication` for details.
+The architecture for rendering glTF scenes is roughly as follows:
+
+#### Setup
+
+- Load scene with `SceneLoader`. This gives you a `Scene` which is basically an ECS paired with a node hierarchy.
+- Have a `RenderContext`. This is responsible for containing global camera information, view and projection matrices,
+ frustum planes (for culling) and lighting setup.
+- Set up a `LightingParameters` struct. Fill in relevant data, and pass it to `RenderContext::set_lighting_parameters()`.
+- Make some Camera, either an `FPSCamera` or pick a camera from the `Scene`.
+- Set up a `Renderer` with a constructor argument depending if it's a forward, deferred or lighting renderer.
+ If using forward, you need to set options using `Renderer::set_mesh_renderer_options_from_lighting()`.
+ This makes sure that the shaders will support all the features required from the Lighting setup.
+
+#### Per-frame
+
+You can modify `Scene::Node` transforms every frame for say, animation.
+Every frame you need to call `Scene::update_cached_transforms()`. This will walk through the node hierarchy and update
+world space `AABB`, world model matrix as well as normal matrices, or the transforms for all bones for skinned meshes.
+
+Also, we need to update the `RenderContext` based on the Camera. `RenderContext::set_camera()` will do this.
+
+Keep a `VisibilityList` around.
+Query the `Scene` for renderables. For forward rendering, you could do something like:
+- `Scene::gather_visible_opaque_renderables()`
+- `Scene::gather_visible_transparent_renderables()`
+- `Scene::gather_unbounded_renderables()`
+
+You pass in the render context and a visibility list, and out comes all the objects you need to render.
+
+To actually render, you would do:
+- `Renderer::begin()`: Resets render queues.
+- `Renderer::push_renderables()`: Calls `AbstractRenderable::get_render_info()` to push data into the render queue.
+For depth-only rendering, use `push_depth_renderables` to get simpler rendering.
+- `Renderer::flush()`: Sorts the queue as appropriate, batches, and submits commands to `CommandBuffer`.
+You will need to be in a render pass (see Vulkan section) to call `flush()`.
+
+It is possible to pass in various flags to `flush()` which controls some common render state.
+
+### Render graph
+
+A powerful system for declaring the rendering you're doing up front, and have the render graph sort out dependencies and synchronization.
+Used by the `SceneViewerApplication`.
+
+### Scene and scene loader
+
+Loads glTF scene, and constructs a `Scene` from it. The scene contains a node hierarchy as well as an Entity Component System to let application query relevant object types.
+
+### Shader suite
+
+The renderer uses a bank of shaders which renderable objects will pull shaders from. To get alternative shaders for glTF meshes and other renderables, this could be modified.
+
+## `scene_formats/`
+
+Deals with glTF file format import and export as well as dealing with compressed textures.
+
+- BC1/3/6/7 is supported through ISPC
+- BC 4/5 is supported by custom code
+- ASTC is supported by ISPC or astcenc
+
+A special purpose texture file format is defined here as well (GTX, Granite Texture Format, totally not confusing to anyone :P).
+It's designed to be loaded directly as a memory mapped file and integrates nicely with the filesystem code.
+
+This used to use GLI to load KTX files, but there was no clean way to pipe it through with `mmap()`
+and compile times soared.
+
+## `tests/`
+
+A couple ad-hoc tests and sandboxy code to test things quick and dirty.
+You can find some API usage examples here.
+
+## `third_party/`
+
+Submodule heaven. All third party submodules are checked out here. See README.md for which submodules are used.
+
+## `threading/`
+
+A fairly straight forward thread pool with task scheduling and dependency tracking. Not used that extensively yet.
+Used for texture loading in the texture manager and for the CPU clustering implementation.
+The idea is to make the render graph automatically thread everything through this, but that's a pretty large TODO.
+
+## `tools/`
+
+Some useful tools for special purposes. Read source for more details.
+
+### `aa-bench`
+
+Benchmarks AA implementations.
+
+### `sweep_*.py`
+
+Benchmarks and analyzes a glTF scene rendered in many different ways.
+
+### `gltf-repacker`
+
+Optimizes a glTF scene.
+Removes duplicate mesh data, quantizes attributes, optimizes meshes, compresses textures (into GTX format), etc.
+
+### `obj-to-gltf`
+
+A crude converter from OBJ to glTF. Needed a special purpose converter once for Sponza.
+
+## `ui/`
+
+A bare-bones retained mode UI experiment.
+
+## `util/`
+
+Various standalone utility classes.
+The most widely used one is the `IntrusivePtr` and `IntrusiveList`, used by the Vulkan backend
+to deal with ref-counted handles.
+
+The object pool is also used extensively. Pairing object pool with intrusive pointer was found
+to be very nifty indeed.
+
+## `viewer/`
+
+A standalone glTF viewer. It uses `SceneViewerApplication` directly.
+
+## `vulkan/`
+
+This is the Vulkan backend and is the part of the code base which needs most
+explanation and rationale behind its design.
+
+### `context.cpp` and `context.hpp`
+
+Here we have the "context". This is the module which:
+
+- Initializes the Vulkan loader
+- Creates a VkInstance
+- Creates a VkDevice
+- Initializes various VkQueues.
+The goal is to find 3 queues.
+One for graphics or "general" workloads,
+one compute queue (ideally async compute in a different queue family),
+and one transfer/DMA queue (ideally a separate DMA engine).
+
+The queues will alias each other if no "ideal" queue can be found.
+The context owns the lifetime for a VkInstance or VkDevice.
+
+Validation layers are also hooked up and reported here.
+
+### `wsi.cpp` and `wsi.hpp`
+
+In the layer below we have the WSI or windowing system integration.
+This module is responsible for managing `VkSurfaceKHR` and `VkSwapchainKHR`.
+Surfaces are created and interacted with through the `WSIPlatform`
+interface which the platform code (or application code) is responsible for implementing.
+
+The module also has support for external swapchains, i.e. a swapchain whose images do not belong to a `VkSwapchainKHR`.
+This is is useful for applications which want to use offscreen rendering only.
+In this case, the implementation can simply pretend it's working with swapchain images
+and provide release semaphores and take acquire semaphores directly from the application.
+
+The WSI owns a `Device` instance as well, so `WSI` is essentially a superset of `Device`.
+Normally, an `Application` instance owns a `WSI` instance as well,
+so applications normally don't need to know about this.
+
+The main entry points into WSI which are called on a per-frame basis is:
+- `WSI::begin_frame()`: Acquires the swapchain, and calls `Device::begin_frame()`
+- `WSI::end_frame()`: Flushes the frame with `Device::end_frame()`. If the swapchain was rendered into,
+`vkQueuePresentKHR` will be called and the backbuffer is flipped on-screen. If the swapchain was not rendered into,
+the implementation will stall with `vkDeviceWaitIdle` as it assumes the application is in some
+kind of loading scenario and old resources should be flushed out.
+
+This is typically called by the platform code via `Application::run_frame()`.
+
+### `device.cpp` and `device.hpp`
+
+This is the main interface to Vulkan, `Vulkan::Device`. Here you:
+- Create and allocate resources
+- Request command buffers
+- Submit command buffers and signal fences and/or semaphores
+- Wait for semaphores, etc
+- Various physical device queries (like format support, etc)
+- Interact with texture and shader managers
+
+#### Per-frame resources
+
+To manage synchronization between CPU and GPU at a higher level, the implementation
+has a concept of frames. Each frame corresponds to a swapchain frame.
+Each frame owns a data structure which serves as a pool of various resources to be deleted or recycled.
+
+If you allocate command buffers, it comes from the pool of the current frame.
+If you delete a resource like a buffer or image, it will be recycled back into
+the per-frame pool, and deleted later.
+
+Each frame has a list of fences to wait for and/or recycle when that frame index begins.
+When a particular frame has begun (usually pumped through by WSI acquire), fences are waited on,
+and deferred actions happen. This will be resetting command pools, descriptor set allocator state updated,
+buffers and images deleted, fences and semaphores recycled, and so on.
+This scheme means we avoid having to think too hard about waiting on GPU to complete stuff
+before we touch them on CPU.
+
+#### Command buffer requests and submissions
+
+To submit work to the GPU, you can request a command buffer with a specific type:
+- Generic
+- Async compute
+- Async graphics (tries to run graphics on the compute queue if it can support it, very special purpose)
+- Transfer
+
+This maps directly to a `VkCommandBuffer`, but has a lot of plumbing around it to make it easy.
+There is a separate section for the details.
+
+When you request a command buffer you lock the current frame, and it cannot end until you submit
+the command buffer. The reasoning for this is multi-threading complicated things
+and the fact that command buffers are owned by a single `VkCommandPool`,
+and thus allocators which are tied to a particular frame.
+I chose not to decouple command buffer allocation from the frame,
+or we would end up with a huge number of separate pools,
+which on some implementations would be pretty terrible.
+
+Locking is only a potential problem with multi-threading if we try to record
+some command buffer completely asynchronously with the swapchain and that operation takes a long time,
+but recording the actual command buffer should be a quick and easy operation.
+If locking ever becomes a problem, this needs to be redesigned a bit.
+The main case where this "async command buffer" recording happens is threaded image uploads which are done by the texture manager.
+However, just recording a simple `copy_buffer_to_image()` isn't the most expensive thing in the world.
+
+#### Resources
+
+Resources which are held by the application are managed through a "smart pointer",
+but it is effectively typedef-ed away. All resource types like Buffer, Image,
+etc implement `IntrusivePtrEnabled`, which embeds a shared_ptr-like control block internally.
+The intent here is that we can easily make it single-threaded ref-count or multi-threaded (atomics),
+by flipping a define or by changing the IntrusivePtrEnabled inheritance.
+
+Resource handle memory is managed through an `ObjectPool`,
+which means freeing and allocating objects should minimize the required heap allocations.
+When the `IntrusivePtr` is destroyed, it recycles itself properly into the respective pools.
+
+#### Allocating GPU memory
+
+Allocating GPU memory is done by a custom heap memory allocator.
+This predates the de-factor memory allocator from AMD by a long while, but it's basically the same concept.
+To the host side, you have some choices when you allocate buffers and images.
+
+Host-visible memory is always persistently mapped with `vkMapMemory()`.
+Mapping and unmapping in the API is basically free,
+except maybe `vkFlushMappedRanges` and similar if you're using incoherent memory.
+
+For buffers you can decide between:
+- Device: The buffer will be kept in `DEVICE_LOCAL` memory.
+It may or may not be `HOST_VISIBLE`.
+- Host: The buffer will be kept in `HOST_VISIBLE | HOST_COHERENT`.
+Designed for upload to the GPU, because it's likely not `CACHED`.
+- CachedHost: `HOST_VISIBLE | HOST_CACHED`. May not be `COHERENT`,
+but the details here are abstracted through `Device::map_host_buffer()`.
+Use it for readbacks from GPU to CPU.
+
+For images you can pick:
+- Physical: Backed by physical memory.
+- Transient: Only backed by on-chip tile memory. Use for g-buffers, etc, although there is a simpler interface
+for requesting transient surfaces, see `Device::get_transient_attachment()`.
+
+E.g.:
+```
+CommandBufferHandle cmd = device->request_command_buffer();
+cmd->set_texture(set = 0, binding = 1, view = my_texture->get_view(), sampler = StockSampler::LinearClamp);
+```
+
+#### Shaders
+
+Shader objects can be requested from the Device by providing a SPIR-V blob.
+Granite will manage these internally and build reflection information using SPIRV-Cross.
+Programs are linked together using multiple Shader objects, this creates the final pipeline layout,
+and we set up descriptor set allocators based on the associated descriptor set layouts.
+
+#### Texture and shader managers
+
+If filesystem support is built in (default), the Granite device also supports
+a texture and shader manager. These allow you to pass in paths, and get handles back.
+Through the magic of inotify, the backing shaders will be recompiled and textures will automatically update themselves.
+The texture manager loads images in the background in the thread pool.
+
+#### Submitting command buffers, signalling sync objects and waiting
+
+You can submit command buffers using `Device::submit()`.
+This will queue up submissions internally until flushed, and submit it as a batch.
+You can pass in either a pointer to `Fence`, and/or `Semaphore`.
+These map directly to `VkFence` and `VkSemaphore` respectively.
+If you signal a fence or semaphore,
+there is an implicit flush to ensure that we don't end up in a wait-before-signal scenario.
+
+Fences can wait on CPU, while semaphores can be waited on using `Device::add_wait_semaphore()` in a particular queue.
+Note that semaphores can only be waited on once (Vulkan restriction), unlike fences.
+
+You can manually flush using `Device::flush_frame()`, wait idle and reclaim all pending resources with `Device::wait_idle()`,
+or only signal fence or semaphores using `Device::submit_empty()`.
+
+### `command_buffer.hpp` and `command_buffer.cpp`
+
+The aim of Granite is a "mid-level" abstraction. Some convenience is allowed at the cost of CPU cycles,
+but not so much that we're back to GL levels of silliness.
+
+#### Barriers and image layouts
+
+Granite does not attempt to perform any synchronization on behalf of the application,
+except for a few isolated cases like `create_buffer()` and `create_image()`,
+where we can just wait on `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT`, and block all possible consumers of the resource.
+Another exception is rendering to the swap chain.
+The backend will wait for the acquire semaphore and deal with layout transitions automatically.
+The other exception is transient images, these are synchronized with `VK_SUBPASS_EXTERNAL` dependencies,
+and implicit layout transitions in a render pass. Applications do not need to synchronize this.
+
+It is your responsibility to synchronize with pipeline barriers, events, or semaphores.
+You will also need to change image layouts manually.
+
+The render graph is designed to remove most of the need to do manual synchronization like this.
+
+Image handles can have one of two layouts "Optimal" and "General". If the image is in Optimal, the command buffer recorder
+will always assume that the image is being used in its related, optimal layout. E.g., when sampling, it must be
+`SHADER_READ_ONLY_OPTIMAL`. In General, the image layout is assumed to always be the catch-all `VK_IMAGE_LAYOUT_GENERAL`.
+
+#### Descriptor set management
+
+Descriptor sets are managed internally.
+Granite uses a more traditional binding model where you bind resources to slots like in GL/D3D11, except,
+to be more Vulkan-like, descriptor sets and bindings are separated. And there is no remapping of bindings.
+On draw call time, Granite will build new descriptor sets for you, or reuse them if it has seen it before.
+The application is freed from the burden of building descriptor sets by hand.
+Descriptor set memory is managed internally as well, and recycled as appropriate.
+
+#### Pipeline management
+
+Granite also uses a more classic way of setting rendering state.
+State can be saved and restored in a more stack-like way which removes most need to set specific rendering state
+all the time in scene rendering. Pipelines are compiled on-demand.
+
+Since on-demand pipeline creation can cause issues, Granite supports pipeline caches, as well as Fossilize, which
+allows us to prewarm the internal hashmaps with VkPipelines ready to go if we so choose.
+
+#### Allocating scratch data (VBO, IBO, UBO)
+
+Sometimes you just need to stream out data and forget about it, like vertex buffers, index buffers, and in particular,
+uniform buffer data. To avoid having to manage this memory explicitly, the command buffers has convenience functions
+to allocate and bind. This allocation is basically free because it's backed by a pool of linear allocators.
+Always use this for streamed data which can be discarded after you're done rendering.
+
+#### Updating textures asynchronously
+
+Similar to creating staging data for VBO, IBO and UBOs, you can do similar kind of updates to textures.
+It will allocate staging data for you, issue `vkCmdCopyBufferToImage` commands and give you a pointer you can write.
+
+#### Drawing
+
+Granite supports the basic draw commands you'd expect.
+On draw, any dirty state or dirty descriptor sets are resolved,
+just like you would expect in an older engine.
+
+#### Render passes
+
+Granite has very explicit render passes, and maps almost 1:1 to Vulkan.
+You fill in a `RenderPassInfo` and call `CommandBuffer::begin_render_pass()` and `end_render_pass()`.
+You can declare a full multipass setup, but you are freed from the burden of figuring out subpass dependencies,
+layout transitions in the render passes, etc.
+
+For the simple non-multipass case, you need to set up:
+- Color attachments w/ count
+- Depth stencil attachment
+- Which attachments should be cleared, and to what color.
+- Which attachments should be loaded to tile.
+- Which attachments should be stored and not discarded.
+- Whether depth-stencil is read-only.
+
+##### Rendering to swap chain
+
+There is a special purpose function to render to the swap chain.
+Use `Device::get_swapchain_render_pass()`.
+You can pick if you want color-only, color/depth or color/depth/stencil attachments.
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/README.md b/crates/pyrowave-sys/vendor/pyrowave/Granite/README.md
new file mode 100644
index 00000000..971b1a68
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/README.md
@@ -0,0 +1,205 @@
+# Granite
+
+Granite is my personal Vulkan renderer project.
+
+## Why release this?
+
+The most interesting part of this project compared to the other open-source Vulkan renderers so far
+is probably the render graph implementation.
+
+The project is on GitHub in the hope it might be useful as-is
+for learning purposes or generating implementation ideas.
+
+### Disclaimer
+
+**Do not expect any support or help.
+Pull requests will likely be ignored or dismissed.**
+
+## License
+
+The code is licensed under MIT. Feel free to use it for whatever purpose.
+
+## High-level documentation
+
+See `OVERVIEW.md`.
+
+## Low-level rendering backend
+
+The rendering backend focuses entirely on Vulkan,
+so it reuses Vulkan enums and data structures where appropriate.
+However, the API greatly simplifies the more painful points of writing straight Vulkan.
+It's not designed to be the fastest renderer ever made, it's likely a happy middle ground between
+"perfect" Vulkan and OpenGL/D3D11 w.r.t. CPU overhead.
+
+- Memory manager
+- Deferred destruction and release of API objects and memory
+- Automatic descriptor set management
+- Linear allocators for vertex/index/uniform/staging data
+- Automatic pipeline creation
+- Command buffer tracks state similar to older APIs
+- Uses TRANSFER-queue on desktop to upload linear-allocated vertex/index/uniform data in bulk
+- Vulkan GLSL for shaders, shaders are compiled in runtime with shaderc
+- Pipeline cache save-to-disk and reload
+- Warm up internal hashmaps with Fossilize
+- Easier-to-use fences and semaphores
+
+Missing bits:
+- Multithreaded rendering
+- Precompile all shaders to optimized SPIR-V
+
+Implementation is found in `vulkan/`.
+
+## High-level rendering backend
+
+A basic scene graph, component system and other higher-level scaffolding lives in `renderer/`.
+This is probably the most unoptimized and naive part.
+
+## PBR renderer
+
+Pretty barebones, half-assed PBR renderer. Very simplified IBL support.
+Fancy rendering is not the real motivation behind this project.
+
+## Post-AA
+
+Fairly straight forward FXAA, SMAA and TAA (no true velocity buffer though).
+
+## Automatic shader recompile and texture reload (Linux/Android only)
+
+Immediately when shaders are modified or textures are changed, the resources are automatically reloaded.
+The implementation uses inotify to do this,
+so it's exclusive to Linux unless a backend is implemented on Windows (no).
+
+## Network VFS
+
+For Linux host and Android device,
+assets and shaders can be pulled over TCP (via ADB port-forwarding) with `network/netfs_server.cpp`.
+Quite convenient.
+
+## Validation
+
+In debug build, LunarG validation layers are enabled.
+Granite is squeaky clean.
+
+## Render graph
+
+`renderer/render_graph.hpp` and `renderer/render_graph.cpp` contains a fairly complete
+render graph. It supports:
+
+- Automatic layout transitions
+- Automatic loadOp/storeOp usage
+- Automatic scaled loadOp for simple lower-res game -> high-res UI rendering scenarios
+- Uses async compute queues automatically
+- Optimal barrier placement, signals as early as possible, waits as late as possible
+ VkEvent is used for in-queue resources, VkSemaphore for cross-queue resources
+- Basic render target aliasing
+- Can merge two or more passes into multiple subpasses for efficient rendering on tile-based architectures
+- Automatic mip-mapping if requested
+- Uses transient attachments automatically to save memory on tile-based architectures
+- Render target history, read previous frame's results in next frame for feedback
+- Conditional render passes, can preserve render passes if necessary
+- Render passes are reordered for optimal (?) overlap in execution
+- Automatic, optimal multisampled resolve with pResolveAttachments
+
+I have written up a longer blog post about its implementation
+[here](http://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/).
+
+The default application scene renderer in `application/application.cpp` sets up
+a render graph which does:
+- Conditionally renders a shadow map covering entire scene
+- Renders a close shadow map
+- Automatically pulls in reflection/refraction render passes if present in the scene graph
+- Renders scene G-Buffer with deferred
+- Lighting pass (merged with G-Buffer pass into a single render pass)
+- Bloom threshold pass
+- Bloom pyramid downsampling
+- Async compute is kicked off to get average luminance of scene, adjusts exposure
+- Two upsampling steps to complete blurring in parallel with async
+- Tonemap (HDR + Bloom) rendered to backbuffer (sRGB)
+- (Potentially UI can be rendered on top with merged subpasses)
+
+## Scene format
+
+glTF 2.0 with PBR materials is mostly supported.
+A custom JSON format is also added in order to plug multiple
+glTF files together for rapid prototyping of test scenes.
+
+## Texture formats
+
+- PNG, JPG, TGA, HDR (via stb)
+- GTX (Granite Texture Format, custom texture format for compressed formats)
+
+ASTC, ETC2 and BCn/DXTn compressed formats are supported.
+
+## `gltf-repacker`
+
+There's a tool to repack glTF models.
+Textures can be compressed to ASTC or BC using ISPC Texture Compressor.
+zeux's meshoptimizer library can also optimize meshes.
+The glTF emitted uses some Granite specific extras to be more optimal, so it's mostly for internal use.
+
+## Compilers
+
+Tested on GCC, Clang, and MSVC 2017.
+
+## Platforms
+
+- SDL3 (Linux / Windows)
+- `VK_KHR_display` (headless Linux w/ basic keyboard, mouse, gamepad support)
+- libretro Vulkan HW interface
+- Headless (benchmarking)
+- Custom surface plugin
+- Android
+
+## Vulkan implementations tested
+
+- AMD Linux (Mesa, AMDVLK)
+- Intel Linux (Mesa)
+- AMD Windows
+- nVidia Linux
+- Arm Mali (Galaxy S7/S8/S9)
+- Pixel C tablet (Tegra X1)
+
+## Build
+
+Plain CMake. Remember to check out submodules with `git submodule update --init`.
+
+```
+mkdir build
+cd build
+cmake .. -DCMAKE_BUILD_TYPE=Release -G Ninja
+ninja -j16 # YMMV :3
+```
+
+For MSVC, it should work to use the appropriate `-G` flag.
+There aren't any real samples yet, so not much to do unless you use Granite as a submodule.
+
+`viewer/gltf-viewer` is a basic glTF viewer used as my sandbox for more complex testing.
+Try some models from glTF-Sample-Models.
+
+### Android
+
+Something ala:
+```
+cd viewer
+gradle build
+```
+Assets used in the default `gltf-viewer` target are pulled from `viewer/assets`.
+
+### Third party software
+
+These are pulled in as submodules.
+
+- [SDL3](https://github.com/libsdl-org/SDL)
+- [glslang](https://github.com/google/glslang.git)
+- [rapidjson](https://github.com/miloyip/rapidjson)
+- [shaderc](https://github.com/google/shaderc.git)
+- [SPIRV-Cross](https://github.com/KhronosGroup/SPIRV-Cross)
+- [SPIRV-Headers](https://github.com/KhronosGroup/SPIRV-Headers)
+- [SPIRV-Tools](https://github.com/KhronosGroup/SPIRV-Tools)
+- [stb](https://github.com/nothings/stb)
+- [volk](https://github.com/zeux/volk)
+- [meshoptimizer](https://github.com/zeux/meshoptimizer)
+- [Fossilize](https://github.com/ValveSoftware/Fossilize)
+- [muFFT](https://github.com/Themaister/muFFT)
+- MikkTSpace (inlined into `third_party/mikktspace`)
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/CMakeLists.txt b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/CMakeLists.txt
new file mode 100644
index 00000000..fd73f51d
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/CMakeLists.txt
@@ -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()
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application.cpp
new file mode 100644
index 00000000..709c75e4
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application.cpp
@@ -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 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(*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();
+ 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(&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(&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());
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application.hpp
new file mode 100644
index 00000000..cdb88fc0
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application.hpp
@@ -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 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 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();
+};
+}
\ No newline at end of file
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_entry.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_entry.cpp
new file mode 100644
index 00000000..b953c256
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_entry.cpp
@@ -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
+#endif
+
+#if defined(_WIN32) && !defined(APPLICATION_ENTRY_HEADLESS)
+#define USE_WINMAIN
+#endif
+
+#ifdef USE_WINMAIN
+#define WIN32_LEAN_AND_MEAN
+#include
+#include
+#include
+#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 argv_buffer(argc + 1);
+ char **argv = nullptr;
+ std::vector 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(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;
+}
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_glue.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_glue.hpp
new file mode 100644
index 00000000..46254f44
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_glue.hpp
@@ -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
+#include
+
+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; } }
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_interface_query.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_interface_query.cpp
new file mode 100644
index 00000000..19a85a1e
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/application_interface_query.cpp
@@ -0,0 +1,2 @@
+#include "application_glue.hpp"
+GRANITE_APPLICATION_DECL_DEFAULT_QUERY()
\ No newline at end of file
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/CMakeLists.txt b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/CMakeLists.txt
new file mode 100644
index 00000000..f2b921f8
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/CMakeLists.txt
@@ -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)
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_events.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_events.hpp
new file mode 100644
index 00000000..5d2e160a
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_events.hpp
@@ -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;
+};
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi.cpp
new file mode 100644
index 00000000..07911446
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi.cpp
@@ -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(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(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(device, index);
+ }
+}
+
+void GraniteWSIPlatform::event_frame_tick(double, double)
+{
+}
+
+template
+void GraniteWSIPlatform::dispatch_template(const T &t)
+{
+ if (auto *em = GRANITE_EVENT_MANAGER())
+ em->dispatch_inline(t);
+}
+
+template
+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
+void GraniteWSIPlatform::dispatch_or_defer(Func &&func)
+{
+ if (in_async_input)
+ captured.emplace_back(std::forward(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();
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi.hpp
new file mode 100644
index 00000000..8232d4c2
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi.hpp
@@ -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
+
+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
+ void dispatch_template_filter(const T &t);
+ template
+ void dispatch_template(const T &t);
+ template
+ void dispatch_or_defer(Func &&func);
+
+ bool in_async_input = false;
+ std::vector> captured;
+};
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi_events.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi_events.hpp
new file mode 100644
index 00000000..8028b309
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/events/application_wsi_events.hpp
@@ -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;
+};
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/CMakeLists.txt b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/CMakeLists.txt
new file mode 100644
index 00000000..4cdcad93
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/CMakeLists.txt
@@ -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})
\ No newline at end of file
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers.cpp
new file mode 100644
index 00000000..36268594
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers.cpp
@@ -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
+#include
+#include
+
+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(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; }
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers.hpp
new file mode 100644
index 00000000..967cd93c
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers.hpp
@@ -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
+#include
+#include
+#include
+#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;
+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())
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_init.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_init.cpp
new file mode 100644
index 00000000..a914ca7b
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_init.cpp
@@ -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(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);
+}
+}
+}
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_init.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_init.hpp
new file mode 100644
index 00000000..ee0af26c
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_init.hpp
@@ -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);
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_interface.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_interface.hpp
new file mode 100644
index 00000000..751c61d3
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/global/global_managers_interface.hpp
@@ -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
+#include
+#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 &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;
+};
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/CMakeLists.txt b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/CMakeLists.txt
new file mode 100644
index 00000000..1f414441
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/CMakeLists.txt
@@ -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()
\ No newline at end of file
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input.cpp
new file mode 100644
index 00000000..0d0acfed
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input.cpp
@@ -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
+#include
+
+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;
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input.hpp
new file mode 100644
index 00000000..fa88c53a
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input.hpp
@@ -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
+#include "math.hpp"
+#include
+#include
+#include
+
+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;
+};
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input_sdl.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input_sdl.cpp
new file mode 100644
index 00000000..bc8aedf8
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input_sdl.cpp
@@ -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;
+ }
+ }
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input_sdl.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input_sdl.hpp
new file mode 100644
index 00000000..cb6a6504
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/input/input_sdl.hpp
@@ -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
+#include "input.hpp"
+#include
+
+namespace Granite
+{
+class InputTrackerSDL
+{
+public:
+ using Dispatcher = std::function)>;
+ 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);
+};
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/.dummy.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/.dummy.cpp
new file mode 100644
index 00000000..e69de29b
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/CMakeLists.txt b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/CMakeLists.txt
new file mode 100644
index 00000000..44601339
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/CMakeLists.txt
@@ -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()
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/build.gradle b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/build.gradle
new file mode 100644
index 00000000..8a50a47d
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/build.gradle
@@ -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'
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/external_layers/README.txt b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/external_layers/README.txt
new file mode 100644
index 00000000..d67c4b85
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/external_layers/README.txt
@@ -0,0 +1 @@
+Place {arm64-v8a,armeabi-v7a}/libVkLayer_*.so here to have it bundled in the APK.
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/AndroidManifest.xml b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/AndroidManifest.xml
new file mode 100644
index 00000000..6af90d3d
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/build.gradle b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/build.gradle
new file mode 100644
index 00000000..1a74f628
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/build.gradle
@@ -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$$
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/gradle.properties b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/gradle.properties
new file mode 100644
index 00000000..5bac8ac5
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/gradle.properties
@@ -0,0 +1 @@
+android.useAndroidX=true
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-hdpi/icon.png b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-hdpi/icon.png
new file mode 100644
index 00000000..83b7a19a
Binary files /dev/null and b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-hdpi/icon.png differ
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-mdpi/icon.png b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-mdpi/icon.png
new file mode 100644
index 00000000..f843f898
Binary files /dev/null and b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-mdpi/icon.png differ
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xhdpi/icon.png b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xhdpi/icon.png
new file mode 100644
index 00000000..2d813876
Binary files /dev/null and b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xhdpi/icon.png differ
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xxhdpi/icon.png b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xxhdpi/icon.png
new file mode 100644
index 00000000..f33b9bf8
Binary files /dev/null and b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xxhdpi/icon.png differ
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xxxhdpi/icon.png b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xxxhdpi/icon.png
new file mode 100644
index 00000000..27c5fea0
Binary files /dev/null and b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/res/drawable-xxxhdpi/icon.png differ
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/settings.gradle b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/settings.gradle
new file mode 100644
index 00000000..fc3e2bdb
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/settings.gradle
@@ -0,0 +1,3 @@
+include 'granite'
+project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
+include '$$APP$$'
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/settings_custom.gradle b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/settings_custom.gradle
new file mode 100644
index 00000000..6739171b
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/settings_custom.gradle
@@ -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$$'
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/toplevel.build.gradle b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/toplevel.build.gradle
new file mode 100644
index 00000000..1cae4dd1
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/toplevel.build.gradle
@@ -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
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/java/net/themaister/granite/GraniteActivity.java b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/java/net/themaister/granite/GraniteActivity.java
new file mode 100644
index 00000000..8d207dec
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/java/net/themaister/granite/GraniteActivity.java
@@ -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;
+ }
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/res/values/strings.xml b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/res/values/strings.xml
new file mode 100644
index 00000000..85420055
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/res/values/strings.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/res/values/themes.xml b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/res/values/themes.xml
new file mode 100644
index 00000000..1bf466d9
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_android.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_android.cpp
new file mode 100644
index 00000000..890f1db2
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_android.cpp
@@ -0,0 +1,1363 @@
+/* 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 "game-activity/GameActivity.h"
+#include "game-activity/native_app_glue/android_native_app_glue.h"
+#include "paddleboat/paddleboat.h"
+#include "logging.hpp"
+#include "application.hpp"
+#include "application_events.hpp"
+#include "application_wsi.hpp"
+#include "context.hpp"
+#include "string_helpers.hpp"
+#include
+#include
+#include
+#if defined(HAVE_SWAPPY)
+#include "swappy/swappyVk.h"
+#endif
+
+#include "android.hpp"
+#include "os_filesystem.hpp"
+#include "rapidjson_wrapper.hpp"
+#include "muglm/muglm_impl.hpp"
+
+#ifdef AUDIO_HAVE_OPENSL
+#include "audio_opensl.hpp"
+#endif
+#ifdef AUDIO_HAVE_OBOE
+#include "audio_oboe.hpp"
+#endif
+
+using namespace Vulkan;
+
+#define SENSOR_GAME_ROTATION_VECTOR 15
+
+namespace Granite
+{
+uint32_t android_api_version;
+void *java_vm;
+
+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);
+}
+
+struct GlobalState
+{
+ android_app *app;
+ int32_t base_width;
+ int32_t base_height;
+ int display_rotation;
+ bool has_window;
+ bool active;
+ bool content_rect_changed;
+};
+
+struct Config
+{
+ unsigned target_width = 0;
+ unsigned target_height = 0;
+ bool support_prerotate = true;
+ bool support_gyro = false;
+};
+
+struct JNI
+{
+ JNIEnv *env;
+ jclass granite;
+ jmethodID getDisplayRotation;
+ jmethodID getAudioNativeSampleRate;
+ jmethodID getAudioNativeBlockFrames;
+ jmethodID getCommandLineArgument;
+ jclass classLoaderClass;
+ jobject classLoader;
+
+ ASensorEventQueue *sensor_queue;
+ const ASensor *rotation_sensor;
+};
+
+static GlobalState global_state;
+static Config global_config;
+static JNI jni;
+
+static void on_window_resized(android_app *app)
+{
+ if (app->window)
+ {
+ auto new_width = ANativeWindow_getWidth(app->window);
+ auto new_height = ANativeWindow_getHeight(app->window);
+ if (new_width != global_state.base_width || new_height != global_state.base_height)
+ {
+ global_state.base_width = new_width;
+ global_state.base_height = new_height;
+ global_state.content_rect_changed = true;
+ }
+ }
+}
+
+static void on_content_rect_changed(GameActivity *, const ARect *rect)
+{
+ global_state.base_width = rect->right - rect->left;
+ global_state.base_height = rect->bottom - rect->top;
+ global_state.content_rect_changed = true;
+ LOGI("Got content rect: %d x %d\n", global_state.base_width, global_state.base_height);
+}
+
+namespace App
+{
+static std::string getCommandLine()
+{
+ std::string result;
+
+ jstring key = jni.env->NewStringUTF("granite");
+ jstring str = static_cast(jni.env->CallObjectMethod(global_state.app->activity->javaGameActivity,
+ jni.getCommandLineArgument,
+ key));
+ if (str)
+ {
+ const char *data = jni.env->GetStringUTFChars(str, nullptr);
+ if (data)
+ {
+ result = data;
+ jni.env->ReleaseStringUTFChars(str, data);
+ }
+ else
+ LOGE("Failed to get JNI string data.\n");
+ }
+ else
+ LOGE("Failed to get JNI string from getCommandLine().\n");
+
+ return result;
+}
+
+#ifdef HAVE_GRANITE_AUDIO
+static int getAudioNativeSampleRate()
+{
+ int ret = jni.env->CallIntMethod(global_state.app->activity->javaGameActivity, jni.getAudioNativeSampleRate);
+ return ret;
+}
+
+static int getAudioNativeBlockFrames()
+{
+ int ret = jni.env->CallIntMethod(global_state.app->activity->javaGameActivity, jni.getAudioNativeBlockFrames);
+ return ret;
+}
+#endif
+
+static int getDisplayRotation()
+{
+ int ret = jni.env->CallIntMethod(global_state.app->activity->javaGameActivity, jni.getDisplayRotation);
+ return ret;
+}
+}
+
+struct WSIPlatformAndroid : Granite::GraniteWSIPlatform
+{
+ bool init(unsigned width_, unsigned height_)
+ {
+ width = width_;
+ height = height_;
+ VK_ASSERT(global_state.base_width && global_state.base_height);
+
+ if (width == 0 && height != 0)
+ {
+ width = unsigned(std::round(float(height) * get_aspect_ratio()));
+ LOGI("Adjusting width to %u pixels based on aspect ratio.\n", width);
+ }
+
+ if (width != 0 && height == 0)
+ {
+ height = unsigned(std::round(float(width) / get_aspect_ratio()));
+ LOGI("Adjusting height to %u pixels based on aspect ratio.\n", height);
+ }
+
+ if (!Vulkan::Context::init_loader(nullptr))
+ {
+ LOGE("Failed to init Vulkan loader.\n");
+ return false;
+ }
+
+ get_input_tracker().set_touch_resolution(width, height);
+ has_window = global_state.has_window;
+ active = global_state.active;
+
+ return true;
+ }
+
+#if defined(HAVE_SWAPPY)
+ VkDevice current_device = VK_NULL_HANDLE;
+#endif
+
+ void event_swapchain_created(Device *device_, VkSwapchainKHR swapchain, unsigned width_, unsigned height_, float aspect_, size_t count_,
+ VkFormat format_, VkColorSpaceKHR color_space_, VkSurfaceTransformFlagBitsKHR transform_) override
+ {
+#if defined(HAVE_SWAPPY)
+ current_device = device_->get_device();
+
+ uint64_t refresh = 0;
+ if (SwappyVk_initAndGetRefreshCycleDuration(jni.env, global_state.app->activity->javaGameActivity,
+ device_->get_physical_device(), device_->get_device(),
+ swapchain, &refresh))
+ {
+ LOGI("Swappy reported refresh duration of %.3f ms.\n", double(refresh) * 1e-6);
+ }
+ else
+ LOGW("Failed to initialize swappy refresh rate.\n");
+
+ SwappyVk_setWindow(current_device, swapchain, global_state.app->window);
+#endif
+
+ Granite::GraniteWSIPlatform::event_swapchain_created(device_, swapchain, width_, height_,
+ aspect_, count_, format_,
+ color_space_, transform_);
+
+ if (transform_ & (VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR |
+ VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR |
+ VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_90_BIT_KHR |
+ VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_270_BIT_KHR))
+ {
+ std::swap(width_, height_);
+ }
+ get_input_tracker().set_touch_resolution(width_, height_);
+ }
+
+ void destroy_swapchain_resources(VkSwapchainKHR swapchain) override
+ {
+ (void)swapchain;
+#if defined(HAVE_SWAPPY)
+ if (current_device && swapchain)
+ {
+ SwappyVk_destroySwapchain(current_device, swapchain);
+ current_device = VK_NULL_HANDLE;
+ }
+#endif
+ }
+
+ void update_orientation();
+ bool alive(Vulkan::WSI &wsi) override;
+ void poll_input() override;
+ void poll_input_async(Granite::InputTrackerHandler *override_handler) override;
+
+ void request_teardown();
+ void gamepad_update(bool async);
+
+ std::vector get_instance_extensions() override
+ {
+ return { "VK_KHR_surface", "VK_KHR_android_surface" };
+ }
+
+ uint32_t get_surface_width() override
+ {
+ return width;
+ }
+
+ uint32_t get_surface_height() override
+ {
+ return height;
+ }
+
+ float get_aspect_ratio() override
+ {
+ return float(global_state.base_width) / global_state.base_height;
+ }
+
+ VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice) override;
+
+ unsigned width = 0, height = 0;
+ Application *app = nullptr;
+ Vulkan::WSI *app_wsi = nullptr;
+ uint64_t active_axes = 0;
+ bool active = false;
+ bool has_window = true;
+ bool wsi_idle = false;
+ bool requesting_teardown = false;
+
+ bool pending_native_window_init = false;
+ bool pending_native_window_term = false;
+ bool pending_config_change = false;
+ bool has_mouse_input = false;
+
+ struct
+ {
+ GameTextInputState state = {};
+ char buffer[1024];
+ } ime = {};
+
+ void begin_soft_keyboard(const std::string &initial) override
+ {
+ if (global_state.app && global_state.app->activity)
+ {
+ // Very unclear from documentation what the lifetime of this struct is.
+ // Just keep it alive until end of time.
+ ime.state.composingRegion.start = SPAN_UNDEFINED;
+ ime.state.composingRegion.end = SPAN_UNDEFINED;
+ ime.state.text_UTF8 = ime.buffer;
+ strncpy(ime.buffer, initial.c_str(), sizeof(ime.buffer) - 1);
+ ime.state.text_length = strlen(ime.state.text_UTF8);
+ // Might be broken w.r.t. unicode?
+ ime.state.selection.start = ime.state.text_length;
+ ime.state.selection.end = ime.state.text_length;
+ GameActivity_setTextInputState(global_state.app->activity, &ime.state);
+ GameActivity_showSoftInput(global_state.app->activity,
+ GAMEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT);
+ }
+ }
+
+ void end_soft_keyboard() override
+ {
+ if (global_state.app && global_state.app->activity)
+ GameActivity_hideSoftInput(global_state.app->activity, GAMEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY);
+ }
+};
+
+static VkSurfaceKHR create_surface_from_native_window(VkInstance instance, ANativeWindow *window)
+{
+ VkSurfaceKHR surface = VK_NULL_HANDLE;
+ VkAndroidSurfaceCreateInfoKHR create_info = { VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR };
+ create_info.window = window;
+
+ auto gpa = Vulkan::Context::get_instance_proc_addr();
+#define SYM(x) PFN_##x p_##x; do { PFN_vkVoidFunction vf = gpa(instance, #x); memcpy(&p_##x, &vf, sizeof(vf)); } while(0)
+ SYM(vkCreateAndroidSurfaceKHR);
+ if (p_vkCreateAndroidSurfaceKHR(instance, &create_info, nullptr, &surface) != VK_SUCCESS)
+ return VK_NULL_HANDLE;
+ return surface;
+}
+
+static void enable_sensors()
+{
+ if (!jni.rotation_sensor || !jni.sensor_queue)
+ return;
+
+ int min_delay = ASensor_getMinDelay(jni.rotation_sensor);
+ ASensorEventQueue_enableSensor(jni.sensor_queue, jni.rotation_sensor);
+ if (ASensorEventQueue_setEventRate(jni.sensor_queue, jni.rotation_sensor, std::max(8000, min_delay)) < 0)
+ LOGE("Failed to set event rate.\n");
+}
+
+static void disable_sensors()
+{
+ if (!jni.rotation_sensor || !jni.sensor_queue)
+ return;
+
+ ASensorEventQueue_disableSensor(jni.sensor_queue, jni.rotation_sensor);
+}
+
+static void handle_sensors()
+{
+ if (!global_state.app->userData)
+ return;
+
+ auto &state = *static_cast(global_state.app->userData);
+
+ if (!ASensorEventQueue_hasEvents(jni.sensor_queue))
+ return;
+
+ ASensorEvent events[64];
+ for (;;)
+ {
+ int count = ASensorEventQueue_getEvents(jni.sensor_queue, events, 64);
+ if (count <= 0)
+ return;
+
+ for (int i = 0; i < count; i++)
+ {
+ auto &event = events[i];
+ if (event.type == SENSOR_GAME_ROTATION_VECTOR)
+ {
+ quat q(event.data[3], -event.data[0], -event.data[1], -event.data[2]);
+
+ // Compensate for different display rotation.
+ if (global_state.display_rotation == 1)
+ {
+ std::swap(q.x, q.y);
+ q.x = -q.x;
+ }
+ else if (global_state.display_rotation == 2)
+ {
+ // Doesn't seem to be possible to trigger this?
+ LOGE("Untested orientation %u!\n", global_state.display_rotation);
+ }
+ else if (global_state.display_rotation == 3)
+ {
+ std::swap(q.x, q.y);
+ q.y = -q.y;
+ }
+
+ static const quat landscape(muglm::one_over_root_two(), muglm::one_over_root_two(), 0.0f, 0.0f);
+ q = conjugate(normalize(q * landscape));
+ state.get_input_tracker().orientation_event(q);
+ }
+ }
+ }
+}
+
+static void engine_handle_input(WSIPlatformAndroid &state)
+{
+ auto *input_buffer = android_app_swap_input_buffers(global_state.app);
+
+ if (!input_buffer)
+ return;
+
+ for (uint32_t i = 0; i < input_buffer->keyEventsCount; i++)
+ {
+ auto &event = input_buffer->keyEvents[i];
+
+ auto action = event.action;
+ auto code = event.keyCode;
+
+ if (Paddleboat_isInitialized())
+ if (Paddleboat_processGameActivityKeyInputEvent(&event, sizeof(event)))
+ continue;
+
+ if (event.source == AINPUT_SOURCE_KEYBOARD)
+ {
+ if (action == AKEY_EVENT_ACTION_DOWN && code == AKEYCODE_BACK)
+ {
+ LOGI("Requesting teardown.\n");
+ state.requesting_teardown = true;
+ }
+ }
+ }
+
+ android_app_clear_key_events(input_buffer);
+
+ for (uint32_t i = 0; i < input_buffer->motionEventsCount; i++)
+ {
+ auto &event = input_buffer->motionEvents[i];
+
+ auto action = event.action & AMOTION_EVENT_ACTION_MASK;
+ auto index = (event.action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
+ auto source = event.source;
+
+ // Paddleboat eats mouse events, and we want to handle them raw.
+ if (source == AINPUT_SOURCE_MOUSE)
+ {
+ // TODO: Does Android have concept of focus?
+ if (!state.has_mouse_input)
+ {
+ state.get_input_tracker().mouse_enter(0.0, 0.0);
+ state.has_mouse_input = true;
+ }
+
+ switch (action)
+ {
+ case AMOTION_EVENT_ACTION_MOVE:
+ case AMOTION_EVENT_ACTION_HOVER_MOVE:
+ {
+ auto x = GameActivityPointerAxes_getX(&event.pointers[index]);
+ auto y = GameActivityPointerAxes_getY(&event.pointers[index]);
+ x /= float(global_state.base_width);
+ y /= float(global_state.base_height);
+ state.get_input_tracker().mouse_move_event_absolute_normalized(x, y);
+ break;
+ }
+
+ case AMOTION_EVENT_ACTION_DOWN:
+ case AMOTION_EVENT_ACTION_POINTER_DOWN:
+ {
+ auto x = GameActivityPointerAxes_getX(&event.pointers[index]);
+ auto y = GameActivityPointerAxes_getY(&event.pointers[index]);
+ x /= float(global_state.base_width);
+ y /= float(global_state.base_height);
+ if (event.buttonState & AMOTION_EVENT_BUTTON_PRIMARY)
+ state.get_input_tracker().mouse_button_event_normalized(MouseButton::Left, x, y, true);
+ if (event.buttonState & AMOTION_EVENT_BUTTON_SECONDARY)
+ state.get_input_tracker().mouse_button_event_normalized(MouseButton::Right, x, y, true);
+ break;
+ }
+
+ case AMOTION_EVENT_ACTION_UP:
+ case AMOTION_EVENT_ACTION_POINTER_UP:
+ {
+ if (!(event.buttonState & AMOTION_EVENT_BUTTON_PRIMARY))
+ state.get_input_tracker().mouse_button_event(MouseButton::Left, false);
+ if (!(event.buttonState & AMOTION_EVENT_BUTTON_SECONDARY))
+ state.get_input_tracker().mouse_button_event(MouseButton::Right, false);
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ continue;
+ }
+
+ if (Paddleboat_isInitialized())
+ if (Paddleboat_processGameActivityMotionInputEvent(&event, sizeof(event)))
+ continue;
+
+ if (source == AINPUT_SOURCE_TOUCHSCREEN)
+ {
+ switch (action)
+ {
+ case AMOTION_EVENT_ACTION_DOWN:
+ case AMOTION_EVENT_ACTION_POINTER_DOWN:
+ {
+ auto x = GameActivityPointerAxes_getX(&event.pointers[index]);
+ auto y = GameActivityPointerAxes_getY(&event.pointers[index]);
+ x /= float(global_state.base_width);
+ y /= float(global_state.base_height);
+ int id = event.pointers[index].id;
+ state.get_input_tracker().on_touch_down(id, x, y);
+ break;
+ }
+
+ case AMOTION_EVENT_ACTION_MOVE:
+ {
+ size_t count = event.pointerCount;
+ for (size_t p = 0; p < count; p++)
+ {
+ // Divide by base_width / base_height?
+ auto x = GameActivityPointerAxes_getX(&event.pointers[p]);
+ auto y = GameActivityPointerAxes_getY(&event.pointers[p]);
+ x /= float(global_state.base_width);
+ y /= float(global_state.base_height);
+ int id = event.pointers[p].id;
+ state.get_input_tracker().on_touch_move(id, x, y);
+ }
+ state.get_input_tracker().dispatch_touch_gesture();
+ break;
+ }
+
+ case AMOTION_EVENT_ACTION_UP:
+ case AMOTION_EVENT_ACTION_POINTER_UP:
+ {
+ auto x = GameActivityPointerAxes_getX(&event.pointers[index]);
+ auto y = GameActivityPointerAxes_getY(&event.pointers[index]);
+ x /= float(global_state.base_width);
+ y /= float(global_state.base_height);
+ int id = event.pointers[index].id;
+ state.get_input_tracker().on_touch_up(id, x, y);
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+ }
+
+ android_app_clear_motion_events(input_buffer);
+}
+
+static void engine_handle_cmd_init(android_app *app, int32_t cmd)
+{
+ switch (cmd)
+ {
+ case APP_CMD_RESUME:
+ {
+ LOGI("Lifecycle resume\n");
+ enable_sensors();
+
+ global_state.active = true;
+ break;
+ }
+
+ case APP_CMD_PAUSE:
+ {
+ LOGI("Lifecycle pause\n");
+ disable_sensors();
+ global_state.active = false;
+ break;
+ }
+
+ case APP_CMD_START:
+ {
+ LOGI("Lifecycle start\n");
+ if (jni.env && Paddleboat_isInitialized())
+ Paddleboat_onStart(jni.env);
+ break;
+ }
+
+ case APP_CMD_STOP:
+ {
+ LOGI("Lifecycle stop\n");
+ if (jni.env && Paddleboat_isInitialized())
+ Paddleboat_onStop(jni.env);
+ break;
+ }
+
+ case APP_CMD_INIT_WINDOW:
+ {
+ global_state.has_window = app->window != nullptr;
+ if (app->window)
+ {
+ LOGI("Init window\n");
+ global_state.base_width = ANativeWindow_getWidth(app->window);
+ global_state.base_height = ANativeWindow_getHeight(app->window);
+ global_state.content_rect_changed = true;
+ }
+
+ global_state.display_rotation = jni.env->CallIntMethod(app->activity->javaGameActivity, jni.getDisplayRotation);
+ break;
+ }
+
+ case APP_CMD_WINDOW_RESIZED:
+ {
+ on_window_resized(app);
+ break;
+ }
+
+ case APP_CMD_CONTENT_RECT_CHANGED:
+ {
+ on_content_rect_changed(app->activity, &app->contentRect);
+ break;
+ }
+
+ default:
+ break;
+ }
+}
+
+static void engine_handle_cmd(android_app *app, int32_t cmd)
+{
+ if (!app->userData)
+ return;
+ auto &state = *static_cast(app->userData);
+
+ switch (cmd)
+ {
+ case APP_CMD_RESUME:
+ {
+ LOGI("Lifecycle resume\n");
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ {
+ e->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ e->enqueue_latched(
+ ApplicationLifecycle::Running);
+ }
+ enable_sensors();
+
+ Granite::Global::start_audio_system();
+
+ state.active = true;
+ if (state.wsi_idle)
+ {
+ state.get_frame_timer().leave_idle();
+ state.wsi_idle = false;
+ }
+ break;
+ }
+
+ case APP_CMD_PAUSE:
+ {
+ LOGI("Lifecycle pause\n");
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ {
+ e->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ e->enqueue_latched(
+ ApplicationLifecycle::Paused);
+ }
+ disable_sensors();
+ Granite::Global::stop_audio_system();
+
+ state.active = false;
+ state.get_frame_timer().enter_idle();
+ state.wsi_idle = true;
+ break;
+ }
+
+ case APP_CMD_START:
+ {
+ LOGI("Lifecycle start\n");
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ {
+ e->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ e->enqueue_latched(
+ ApplicationLifecycle::Paused);
+ }
+ if (jni.env && Paddleboat_isInitialized())
+ Paddleboat_onStart(jni.env);
+ break;
+ }
+
+ case APP_CMD_STOP:
+ {
+ LOGI("Lifecycle stop\n");
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ {
+ e->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ e->enqueue_latched(
+ ApplicationLifecycle::Stopped);
+ }
+ if (jni.env && Paddleboat_isInitialized())
+ Paddleboat_onStop(jni.env);
+ break;
+ }
+
+ case APP_CMD_INIT_WINDOW:
+ if (app->window != nullptr)
+ {
+ state.has_window = true;
+ LOGI("New window size %d x %d\n", global_state.base_width, global_state.base_height);
+ global_state.base_width = ANativeWindow_getWidth(app->window);
+ global_state.base_height = ANativeWindow_getHeight(app->window);
+ global_state.content_rect_changed = false;
+
+ if (state.app_wsi)
+ {
+ LOGI("Lifecycle init window.\n");
+ auto surface = create_surface_from_native_window(state.app_wsi->get_context().get_instance(), app->window);
+ state.app_wsi->reinit_surface_and_swapchain(surface);
+ }
+ else
+ {
+ LOGI("Pending init window.\n");
+ state.pending_native_window_init = true;
+ }
+ }
+ break;
+
+ case APP_CMD_TERM_WINDOW:
+ state.has_window = false;
+ if (state.app_wsi)
+ {
+ LOGI("Lifecycle deinit window.\n");
+ state.app_wsi->deinit_surface_and_swapchain();
+ }
+ else
+ {
+ LOGI("Pending deinit window.\n");
+ state.pending_native_window_term = true;
+ }
+ break;
+
+ case APP_CMD_WINDOW_RESIZED:
+ {
+ on_window_resized(app);
+ break;
+ }
+
+ case APP_CMD_CONTENT_RECT_CHANGED:
+ {
+ on_content_rect_changed(app->activity, &app->contentRect);
+ break;
+ }
+
+ default:
+ break;
+ }
+}
+
+VkSurfaceKHR WSIPlatformAndroid::create_surface(VkInstance instance, VkPhysicalDevice)
+{
+ return create_surface_from_native_window(instance, global_state.app->window);
+}
+
+void WSIPlatformAndroid::update_orientation()
+{
+ global_state.display_rotation = App::getDisplayRotation();
+ LOGI("Got new rotation: %d\n", global_state.display_rotation);
+ LOGI("Got new resolution: %d x %d\n", global_state.base_width, global_state.base_height);
+ pending_config_change = true;
+}
+
+void WSIPlatformAndroid::request_teardown()
+{
+ requesting_teardown = true;
+}
+
+void WSIPlatformAndroid::gamepad_update(bool async)
+{
+ // Don't deal with anything JNI related in async callbacks.
+ if (!async)
+ {
+ if (jni.env && Paddleboat_isInitialized())
+ Paddleboat_update(jni.env);
+
+ // Need to explicitly enables axes we care about.
+ const uint64_t new_active_axes = Paddleboat_getActiveAxisMask();
+ uint64_t new_axes = new_active_axes ^ active_axes;
+
+ if (new_axes != 0)
+ {
+ active_axes = new_active_axes;
+ int32_t axis_index = 0;
+
+ while (new_axes != 0)
+ {
+ if ((new_axes & 1) != 0)
+ {
+ LOGI("Enable Axis: %d", axis_index);
+ GameActivityPointerAxes_enableAxis(axis_index);
+ }
+ axis_index++;
+ new_axes >>= 1;
+ }
+ }
+ }
+
+ auto &tracker = get_input_tracker();
+ for (int i = 0; i < PADDLEBOAT_MAX_CONTROLLERS; i++)
+ {
+ if (Paddleboat_getControllerStatus(i) != PADDLEBOAT_CONTROLLER_ACTIVE)
+ continue;
+
+ Paddleboat_Controller_Info info = {};
+ Paddleboat_getControllerInfo(i, &info);
+ bool known_layout = false;
+
+ switch (info.controllerFlags & PADDLEBOAT_CONTROLLER_LAYOUT_MASK)
+ {
+ case PADDLEBOAT_CONTROLLER_LAYOUT_SHAPES:
+ case PADDLEBOAT_CONTROLLER_LAYOUT_STANDARD:
+ known_layout = true;
+ break;
+
+ default:
+ break;
+ }
+
+ if (!known_layout)
+ continue;
+
+ Paddleboat_Controller_Data data = {};
+ Paddleboat_getControllerData(i, &data);
+
+ struct Mapping
+ {
+ JoypadKey key;
+ uint32_t mask;
+ };
+ static const Mapping map[] = {
+ { JoypadKey::Left, PADDLEBOAT_BUTTON_DPAD_LEFT },
+ { JoypadKey::Right, PADDLEBOAT_BUTTON_DPAD_RIGHT },
+ { JoypadKey::Up, PADDLEBOAT_BUTTON_DPAD_UP },
+ { JoypadKey::Down, PADDLEBOAT_BUTTON_DPAD_DOWN },
+ { JoypadKey::West, PADDLEBOAT_BUTTON_X },
+ { JoypadKey::East, PADDLEBOAT_BUTTON_B },
+ { JoypadKey::North, PADDLEBOAT_BUTTON_Y },
+ { JoypadKey::South, PADDLEBOAT_BUTTON_A },
+ { JoypadKey::Start, PADDLEBOAT_BUTTON_START },
+ { JoypadKey::Select, PADDLEBOAT_BUTTON_SELECT },
+ { JoypadKey::LeftShoulder, PADDLEBOAT_BUTTON_L1 },
+ { JoypadKey::RightShoulder, PADDLEBOAT_BUTTON_R1 },
+ { JoypadKey::LeftThumb, PADDLEBOAT_BUTTON_L3 },
+ { JoypadKey::RightThumb, PADDLEBOAT_BUTTON_R3 },
+ { JoypadKey::Mode, PADDLEBOAT_BUTTON_SYSTEM },
+ };
+
+ for (auto &m : map)
+ {
+ tracker.joypad_key_state(i, m.key,
+ (data.buttonsDown & m.mask) != 0 ?
+ JoypadKeyState::Pressed : JoypadKeyState::Released);
+ }
+
+ tracker.joyaxis_state(i, JoypadAxis::LeftX, data.leftStick.stickX);
+ tracker.joyaxis_state(i, JoypadAxis::LeftY, data.leftStick.stickY);
+ tracker.joyaxis_state(i, JoypadAxis::RightX, data.rightStick.stickX);
+ tracker.joyaxis_state(i, JoypadAxis::RightY, data.rightStick.stickY);
+ tracker.joyaxis_state(i, JoypadAxis::LeftTrigger, data.triggerL2);
+ tracker.joyaxis_state(i, JoypadAxis::RightTrigger, data.triggerR2);
+ }
+}
+
+extern "C"
+{
+static void game_text_input_cb(void *userdata, const struct GameTextInputState *state)
+{
+ auto *app = static_cast(userdata);
+ if (!app || !state)
+ return;
+
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ e->enqueue(state->text_UTF8);
+
+ // Clear the text input flag.
+ app->textInputState = 0;
+}
+}
+
+void WSIPlatformAndroid::poll_input()
+{
+ std::lock_guard holder{get_input_tracker().get_lock()};
+ int events;
+ int ident;
+ android_poll_source *source;
+ app_wsi = nullptr;
+
+ while ((ident = ALooper_pollOnce(0, nullptr, &events, reinterpret_cast(&source))) > ALOOPER_POLL_TIMEOUT)
+ {
+ if (source)
+ source->process(global_state.app, source);
+
+ if (ident == LOOPER_ID_USER)
+ handle_sensors();
+
+ if (global_state.app->destroyRequested)
+ return;
+ }
+
+ if (global_state.app->textInputState)
+ {
+ GameActivity_getTextInputState(global_state.app->activity, game_text_input_cb,
+ global_state.app);
+ }
+
+ engine_handle_input(*this);
+ gamepad_update(false);
+ get_input_tracker().dispatch_current_state(get_frame_timer().get_frame_time());
+}
+
+void WSIPlatformAndroid::poll_input_async(Granite::InputTrackerHandler *override_handler)
+{
+ std::lock_guard holder{get_input_tracker().get_lock()};
+ begin_async_input_handling();
+ {
+ // Only process input events here, not anything related to lifetimes.
+ engine_handle_input(*this);
+ gamepad_update(true);
+ }
+ end_async_input_handling();
+ get_input_tracker().dispatch_current_state(0.0, override_handler);
+}
+
+bool WSIPlatformAndroid::alive(Vulkan::WSI &wsi)
+{
+ auto &state = *static_cast(global_state.app->userData);
+ int events;
+ int ident;
+ android_poll_source *source;
+ state.app_wsi = &wsi;
+
+ if (global_state.app->destroyRequested || requesting_teardown)
+ return false;
+
+ bool once = false;
+
+ if (global_state.content_rect_changed)
+ {
+ update_orientation();
+ global_state.content_rect_changed = false;
+ }
+
+ if (state.pending_config_change)
+ {
+ state.pending_native_window_term = true;
+ state.pending_native_window_init = true;
+ state.pending_config_change = false;
+ }
+
+ const auto flush_pending = [&]() {
+ if (state.pending_native_window_term)
+ {
+ LOGI("Pending native window term\n");
+ wsi.deinit_surface_and_swapchain();
+ state.pending_native_window_term = false;
+ }
+
+ if (state.pending_native_window_init)
+ {
+ LOGI("Pending native window init\n");
+ auto surface = create_surface_from_native_window(wsi.get_context().get_instance(), global_state.app->window);
+ wsi.reinit_surface_and_swapchain(surface);
+ state.pending_native_window_init = false;
+ }
+ };
+
+ flush_pending();
+
+ while (!once || !state.active || !state.has_window)
+ {
+ while ((ident = ALooper_pollOnce((state.has_window && state.active) ? 0 : -1,
+ nullptr, &events, reinterpret_cast(&source))) > ALOOPER_POLL_TIMEOUT)
+ {
+ if (source)
+ source->process(global_state.app, source);
+
+ if (ident == LOOPER_ID_USER)
+ handle_sensors();
+
+ if (global_state.app->destroyRequested)
+ return false;
+ }
+
+ once = true;
+ }
+
+ flush_pending();
+
+ return true;
+}
+
+static void deinit_jni()
+{
+ if (jni.env && Paddleboat_isInitialized())
+ Paddleboat_destroy(jni.env);
+
+ if (jni.env && global_state.app)
+ {
+ global_state.app->activity->vm->DetachCurrentThread();
+ jni.env = nullptr;
+ }
+}
+
+void paddleboat_controller_status_cb(
+ const int32_t controllerIndex,
+ const Paddleboat_ControllerStatus controllerStatus, void *)
+{
+ if (controllerStatus == PADDLEBOAT_CONTROLLER_JUST_CONNECTED)
+ {
+ char name[1024];
+ *name = '\0';
+ Paddleboat_getControllerName(controllerIndex, sizeof(name), name);
+ LOGI("Controller #%u (%s) connected.\n", controllerIndex, name);
+ auto *platform = static_cast(global_state.app->userData);
+ if (platform)
+ platform->get_input_tracker().enable_joypad(controllerIndex, 0, 0 /* todo */);
+
+ }
+ else if (controllerStatus == PADDLEBOAT_CONTROLLER_JUST_DISCONNECTED)
+ {
+ LOGI("Controller #%u disconnected.\n", controllerIndex);
+ auto *platform = static_cast(global_state.app->userData);
+ if (platform)
+ platform->get_input_tracker().disable_joypad(controllerIndex, 0, 0 /* todo */);
+ }
+}
+
+static void init_jni()
+{
+ auto *app = global_state.app;
+ app->activity->vm->AttachCurrentThread(&jni.env, nullptr);
+ java_vm = app->activity->vm;
+
+ if (Paddleboat_init(jni.env, app->activity->javaGameActivity) != PADDLEBOAT_NO_ERROR)
+ LOGE("Failed to initialize Paddleboat.\n");
+ else if (!Paddleboat_isInitialized())
+ LOGE("Paddleboat is not initialized.\n");
+ else
+ Paddleboat_setControllerStatusCallback(paddleboat_controller_status_cb, nullptr);
+
+ jclass clazz = jni.env->GetObjectClass(app->activity->javaGameActivity);
+ jmethodID getApplication = jni.env->GetMethodID(clazz, "getApplication", "()Landroid/app/Application;");
+ jobject application = jni.env->CallObjectMethod(app->activity->javaGameActivity, getApplication);
+
+ jclass applicationClass = jni.env->GetObjectClass(application);
+ jmethodID getApplicationContext = jni.env->GetMethodID(applicationClass, "getApplicationContext", "()Landroid/content/Context;");
+ jobject context = jni.env->CallObjectMethod(application, getApplicationContext);
+
+ jclass contextClass = jni.env->GetObjectClass(context);
+ jmethodID getClassLoader = jni.env->GetMethodID(contextClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
+ jni.classLoader = jni.env->CallObjectMethod(context, getClassLoader);
+
+ jni.classLoaderClass = jni.env->GetObjectClass(jni.classLoader);
+ jmethodID loadClass = jni.env->GetMethodID(jni.classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
+
+ jstring granite_str = jni.env->NewStringUTF("net.themaister.granite.GraniteActivity");
+ jni.granite = static_cast(jni.env->CallObjectMethod(jni.classLoader, loadClass, granite_str));
+
+ jni.getDisplayRotation = jni.env->GetMethodID(jni.granite, "getDisplayRotation", "()I");
+ jni.getAudioNativeSampleRate = jni.env->GetMethodID(jni.granite, "getAudioNativeSampleRate", "()I");
+ jni.getAudioNativeBlockFrames = jni.env->GetMethodID(jni.granite, "getAudioNativeBlockFrames", "()I");
+ jni.getCommandLineArgument = jni.env->GetMethodID(jni.granite, "getCommandLineArgument", "(Ljava/lang/String;)Ljava/lang/String;");
+
+#ifdef HAVE_GRANITE_AUDIO
+ int sample_rate = App::getAudioNativeSampleRate();
+ int block_frames = App::getAudioNativeBlockFrames();
+#ifdef AUDIO_HAVE_OBOE
+ Granite::Audio::set_oboe_low_latency_parameters(sample_rate, block_frames);
+#endif
+#endif
+
+ GameActivity_setWindowFlags(app->activity,
+ AWINDOW_FLAG_KEEP_SCREEN_ON | AWINDOW_FLAG_TURN_SCREEN_ON |
+ AWINDOW_FLAG_FULLSCREEN |
+ AWINDOW_FLAG_SHOW_WHEN_LOCKED,
+ 0);
+}
+
+static void init_sensors()
+{
+ auto *manager = ASensorManager_getInstanceForPackage("net.themaister.GraniteActivity");
+ if (!manager)
+ return;
+
+ jni.rotation_sensor = ASensorManager_getDefaultSensor(manager, SENSOR_GAME_ROTATION_VECTOR);
+ if (!jni.rotation_sensor)
+ return;
+
+ LOGI("Game Sensor name: %s\n", ASensor_getName(jni.rotation_sensor));
+
+ jni.sensor_queue = ASensorManager_createEventQueue(manager, ALooper_forThread(), LOOPER_ID_USER, nullptr, nullptr);
+ if (!jni.sensor_queue)
+ return;
+}
+}
+
+using namespace Granite;
+
+static void wait_for_complete_teardown(android_app *app)
+{
+ // If we requested to be torn down with GameActivity_finish(),
+ // at least make sure we observe and pump through all takedown events,
+ // or we get a deadlock.
+ while (!app->destroyRequested)
+ {
+ android_poll_source *source = nullptr;
+ int events = 0;
+
+ if (ALooper_pollOnce(-1, nullptr, &events, reinterpret_cast(&source)) > ALOOPER_POLL_TIMEOUT)
+ {
+ if (source)
+ source->process(app, source);
+ }
+ }
+
+ assert(app->activityState == APP_CMD_STOP);
+}
+
+static bool key_event_filter(const GameActivityKeyEvent *event)
+{
+ // For some inexplicable reason, this can be a bitmask of GAMEPAD and KEYBOARD ...
+ if (event->source & (AINPUT_SOURCE_GAMEPAD & AINPUT_SOURCE_ANY))
+ return true;
+
+ if (event->source & (AINPUT_SOURCE_KEYBOARD & AINPUT_SOURCE_ANY))
+ {
+ // System level keycodes that we don't care about
+ // should be handled by system.
+ auto code = event->keyCode;
+ bool handled = code != AKEYCODE_VOLUME_DOWN &&
+ code != AKEYCODE_VOLUME_UP;
+ return handled;
+ }
+
+ return false;
+}
+
+static bool motion_event_filter(const GameActivityMotionEvent *event)
+{
+ return (event->source &
+ (AINPUT_SOURCE_TOUCHSCREEN | AINPUT_SOURCE_JOYSTICK | AINPUT_SOURCE_MOUSE) &
+ AINPUT_SOURCE_ANY) != 0;
+}
+
+static void parse_config()
+{
+ std::string android_config;
+ GRANITE_FILESYSTEM()->read_file_to_string("assets://android.json", android_config);
+
+ if (!android_config.empty())
+ {
+ rapidjson::Document doc;
+ doc.Parse(android_config);
+
+ if (doc.HasMember("width"))
+ global_config.target_width = doc["width"].GetUint();
+ if (doc.HasMember("height"))
+ global_config.target_height = doc["height"].GetUint();
+ if (doc.HasMember("supportPrerotate"))
+ global_config.support_prerotate = doc["supportPrerotate"].GetBool();
+ if (doc.HasMember("enableGyro"))
+ global_config.support_gyro = doc["enableGyro"].GetBool();
+ }
+}
+
+void android_main(android_app *app)
+{
+ // Statics on Android might not be cleared out.
+ global_state = {};
+ global_config = {};
+ jni = {};
+
+ global_state.app = app;
+
+ init_jni();
+
+ ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
+ query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
+ Global::init(flags.manager_feature_flags | Global::MANAGER_FEATURE_FILESYSTEM_BIT);
+
+ LOGI("Starting Granite!\n");
+
+#ifdef ANDROID_APK_FILESYSTEM
+
+#ifndef ANDROID_BUILTIN_ASSET_PATH
+#define ANDROID_BUILTIN_ASSET_PATH ""
+#endif
+
+#ifndef ANDROID_ASSET_PATH
+#define ANDROID_ASSET_PATH ""
+#endif
+
+#ifndef ANDROID_FSR2_ASSET_PATH
+#define ANDROID_FSR2_ASSET_PATH ""
+#endif
+
+ AssetManagerFilesystem::global_asset_manager = app->activity->assetManager;
+ if (auto *fs = GRANITE_FILESYSTEM())
+ {
+ fs->register_protocol("builtin", std::make_unique(
+ ANDROID_BUILTIN_ASSET_PATH));
+ fs->register_protocol("assets", std::make_unique(
+ ANDROID_ASSET_PATH));
+ fs->register_protocol("fsr2", std::make_unique(
+ ANDROID_FSR2_ASSET_PATH));
+ fs->register_protocol("cache", std::make_unique(
+ app->activity->internalDataPath));
+ fs->register_protocol("external", std::make_unique(
+ app->activity->externalDataPath));
+
+ parse_config();
+ }
+#endif
+
+ android_app_set_key_event_filter(app, key_event_filter);
+ android_app_set_motion_event_filter(app, motion_event_filter);
+ app->onAppCmd = engine_handle_cmd_init;
+ app->userData = nullptr;
+
+ if (global_config.support_gyro)
+ init_sensors();
+
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ e->enqueue_latched(ApplicationLifecycle::Stopped);
+
+ for (;;)
+ {
+ int events;
+ int ident;
+ android_poll_source *source;
+ while ((ident = ALooper_pollOnce(-1, nullptr, &events, reinterpret_cast(&source))) > ALOOPER_POLL_TIMEOUT)
+ {
+ if (source)
+ source->process(app, source);
+
+ if (app->destroyRequested)
+ {
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ e->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ Global::deinit();
+ deinit_jni();
+ return;
+ }
+
+ if (ident == LOOPER_ID_USER)
+ handle_sensors();
+
+ if (Granite::global_state.has_window && Granite::global_state.active &&
+ Granite::global_state.content_rect_changed)
+ {
+ Granite::global_state.content_rect_changed = false;
+ app->onAppCmd = Granite::engine_handle_cmd;
+
+ try
+ {
+ std::vector argv;
+ argv.push_back("granite");
+
+ std::string cli_arguments = App::getCommandLine();
+ std::vector arguments;
+ LOGI("Intent arguments: %s\n", cli_arguments.c_str());
+ if (!cli_arguments.empty())
+ {
+ arguments = Util::split_no_empty(cli_arguments, " ");
+ for (auto &arg : arguments)
+ {
+ LOGI("Command line argument: %s\n", arg.c_str());
+ argv.push_back(arg.c_str());
+ }
+ }
+ argv.push_back(nullptr);
+
+ auto app_handle = std::unique_ptr(
+ Granite::application_create(int(argv.size()) - 1,
+ const_cast(argv.data())));
+
+ int ret;
+ if (app_handle)
+ {
+ LOGI("Using resolution: %u x %u\n", global_config.target_width, global_config.target_height);
+ app_handle->get_wsi().set_support_prerotate(global_config.support_prerotate);
+
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ {
+ e->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ e->enqueue_latched(
+ ApplicationLifecycle::Paused);
+ e->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ e->enqueue_latched(
+ ApplicationLifecycle::Running);
+ }
+
+ auto platform = std::make_unique();
+ if (platform->init(global_config.target_width, global_config.target_height))
+ {
+ global_state.app->userData = platform.get();
+ if (!app_handle->init_platform(std::move(platform)) || !app_handle->init_wsi())
+ ret = 1;
+ else
+ {
+ // Defer initializing audio until the application has loaded.
+ Granite::Global::start_audio_system();
+
+ while (app_handle->poll())
+ app_handle->run_frame();
+ ret = 0;
+ }
+ }
+ else
+ ret = 1;
+ }
+ else
+ {
+ global_state.app->userData = nullptr;
+ ret = 1;
+ }
+
+ LOGI("Application returned %d.\n", ret);
+ if (auto *e = GRANITE_EVENT_MANAGER())
+ e->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ GameActivity_finish(global_state.app->activity);
+
+ wait_for_complete_teardown(global_state.app);
+
+ app_handle.reset();
+ Global::deinit();
+ deinit_jni();
+ global_state.app->userData = nullptr;
+ return;
+ }
+ catch (const std::exception &e)
+ {
+ LOGE("Application threw exception: %s\n", e.what());
+ deinit_jni();
+ exit(1);
+ }
+ }
+ }
+ }
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_headless.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_headless.cpp
new file mode 100644
index 00000000..5f47ff05
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_headless.cpp
@@ -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
+#include
+#include
+#include "stb_image_write.h"
+#include "cli_parser.hpp"
+#include "os_filesystem.hpp"
+#include "rapidjson_wrapper.hpp"
+#include
+#include
+#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(ApplicationLifecycle::Paused);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(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 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 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 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(ApplicationLifecycle::Stopped);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(ApplicationLifecycle::Paused);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(ApplicationLifecycle::Running);
+ }
+
+ return true;
+ }
+
+ bool init_headless(Application *app_)
+ {
+ app = app_;
+
+ auto context = Util::make_handle();
+
+ 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(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 record_stream;
+#endif
+
+ std::vector swapchain_images;
+ std::vector readback_buffers;
+ std::vector acquire_semaphore;
+ std::string next_readback_path;
+ TaskGroupHandle swapchain_tasks[SwapchainImages];
+ TaskGroupHandle last_task_dependency;
+
+#ifdef HAVE_GRANITE_FFMPEG
+ VideoEncoder encoder;
+ std::vector 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(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 ] [--stat ]\n"
+ "[--fs-assets ] [--fs-cache ] [--fs-builtin ]\n"
+ "[--video-encode-path ]\n"
+ "[--png-reference-path ] [--frames ] [--width ] [--height ] [--time-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(args.assets));
+ if (!args.builtin.empty())
+ GRANITE_FILESYSTEM()->register_protocol("builtin", std::make_unique(args.builtin));
+ if (!args.cache.empty())
+ GRANITE_FILESYSTEM()->register_protocol("cache", std::make_unique(args.cache));
+ }
+
+ auto app = std::unique_ptr(create_application(argc, argv));
+
+ if (app)
+ {
+ auto platform = std::make_unique();
+ 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 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 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;
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_headless_wrapper.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_headless_wrapper.cpp
new file mode 100644
index 00000000..530d1d64
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_headless_wrapper.cpp
@@ -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);
+}
+}
\ No newline at end of file
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro.cpp
new file mode 100644
index 00000000..1ff1cc40
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro.cpp
@@ -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 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 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 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(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("libretro-granite"),
+ const_cast(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()))
+ {
+ 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);
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro_utils.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro_utils.cpp
new file mode 100644
index 00000000..50d2220b
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro_utils.cpp
@@ -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::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::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 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(ApplicationLifecycle::Stopped);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(ApplicationLifecycle::Paused);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(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(ApplicationLifecycle::Paused);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(ApplicationLifecycle::Stopped);
+ }
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro_utils.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro_utils.hpp
new file mode 100644
index 00000000..92589bf4
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_libretro_utils.hpp
@@ -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();
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_null.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_null.cpp
new file mode 100644
index 00000000..94090150
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_null.cpp
@@ -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
+
+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;
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_sdl3.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_sdl3.cpp
new file mode 100644
index 00000000..b3f218f4
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/application_sdl3.cpp
@@ -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
+#include
+#include
+
+#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
+#endif
+
+#ifdef __linux__
+#include
+#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 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(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(SDL_GetPointerProperty(props, "SDL.window.win32.hwnd", nullptr));
+ SDL_UnlockProperties(props);
+ return reinterpret_cast(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(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 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 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 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 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 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(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(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(ApplicationLifecycle::Stopped);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(ApplicationLifecycle::Paused);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(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(ApplicationLifecycle::Paused);
+ em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
+ em->enqueue_latched(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(current_hmonitor);
+ }
+#endif
+
+ template
+ void push_task_to_async_thread(Op &&op)
+ {
+ push_task_to_list(task_list_async, std::forward(op));
+ }
+
+ template
+ void push_non_pollable_task_to_async_thread(Op &&op)
+ {
+ push_non_pollable_task_to_list(task_list_async, std::forward(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 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> list;
+ std::vector> non_pollable_list;
+ } task_list_main, task_list_async;
+
+ static void process_events_for_list(TaskList &list, bool blocking)
+ {
+ std::unique_lock 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
+ void push_task_to_list(TaskList &list, Op &&op)
+ {
+ std::lock_guard holder{list.lock};
+ list.list.emplace_back(std::forward(op));
+ list.cond.notify_one();
+ }
+
+ template
+ void push_non_pollable_task_to_list(TaskList &list, Op &&op)
+ {
+ std::lock_guard holder{list.lock};
+ list.non_pollable_list.emplace_back(std::forward(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 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
+ void push_task_to_main_thread(Op &&op)
+ {
+ push_task_to_list(task_list_main, std::forward(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(create_application(argc, argv));
+ int ret;
+
+ if (app)
+ {
+ auto platform = std::make_unique(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;
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/libretro/libretro.h b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/libretro/libretro.h
new file mode 100644
index 00000000..3d6df6f9
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/libretro/libretro.h
@@ -0,0 +1,3964 @@
+/* Copyright (C) 2010-2020 The RetroArch team
+ *
+ * ---------------------------------------------------------------------------------------
+ * The following license statement only applies to this libretro API header (libretro.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_H__
+#define LIBRETRO_H__
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef __cplusplus
+#if defined(_MSC_VER) && _MSC_VER < 1800 && !defined(SN_TARGET_PS3)
+/* Hack applied for MSVC when compiling in C89 mode
+ * as it isn't C99-compliant. */
+#define bool unsigned char
+#define true 1
+#define false 0
+#else
+#include
+#endif
+#endif
+
+#ifndef RETRO_CALLCONV
+# if defined(__GNUC__) && defined(__i386__) && !defined(__x86_64__)
+# define RETRO_CALLCONV __attribute__((cdecl))
+# elif defined(_MSC_VER) && defined(_M_X86) && !defined(_M_X64)
+# define RETRO_CALLCONV __cdecl
+# else
+# define RETRO_CALLCONV /* all other platforms only have one calling convention each */
+# endif
+#endif
+
+#ifndef RETRO_API
+# if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
+# ifdef RETRO_IMPORT_SYMBOLS
+# ifdef __GNUC__
+# define RETRO_API RETRO_CALLCONV __attribute__((__dllimport__))
+# else
+# define RETRO_API RETRO_CALLCONV __declspec(dllimport)
+# endif
+# else
+# ifdef __GNUC__
+# define RETRO_API RETRO_CALLCONV __attribute__((__dllexport__))
+# else
+# define RETRO_API RETRO_CALLCONV __declspec(dllexport)
+# endif
+# endif
+# else
+# if defined(__GNUC__) && __GNUC__ >= 4
+# define RETRO_API RETRO_CALLCONV __attribute__((__visibility__("default")))
+# else
+# define RETRO_API RETRO_CALLCONV
+# endif
+# endif
+#endif
+
+/* Used for checking API/ABI mismatches that can break libretro
+ * implementations.
+ * It is not incremented for compatible changes to the API.
+ */
+#define RETRO_API_VERSION 1
+
+/*
+ * Libretro's fundamental device abstractions.
+ *
+ * Libretro's input system consists of some standardized device types,
+ * such as a joypad (with/without analog), mouse, keyboard, lightgun
+ * and a pointer.
+ *
+ * The functionality of these devices are fixed, and individual cores
+ * map their own concept of a controller to libretro's abstractions.
+ * This makes it possible for frontends to map the abstract types to a
+ * real input device, and not having to worry about binding input
+ * correctly to arbitrary controller layouts.
+ */
+
+#define RETRO_DEVICE_TYPE_SHIFT 8
+#define RETRO_DEVICE_MASK ((1 << RETRO_DEVICE_TYPE_SHIFT) - 1)
+#define RETRO_DEVICE_SUBCLASS(base, id) (((id + 1) << RETRO_DEVICE_TYPE_SHIFT) | base)
+
+/* Input disabled. */
+#define RETRO_DEVICE_NONE 0
+
+/* The JOYPAD is called RetroPad. It is essentially a Super Nintendo
+ * controller, but with additional L2/R2/L3/R3 buttons, similar to a
+ * PS1 DualShock. */
+#define RETRO_DEVICE_JOYPAD 1
+
+/* The mouse is a simple mouse, similar to Super Nintendo's mouse.
+ * X and Y coordinates are reported relatively to last poll (poll callback).
+ * It is up to the libretro implementation to keep track of where the mouse
+ * pointer is supposed to be on the screen.
+ * The frontend must make sure not to interfere with its own hardware
+ * mouse pointer.
+ */
+#define RETRO_DEVICE_MOUSE 2
+
+/* KEYBOARD device lets one poll for raw key pressed.
+ * It is poll based, so input callback will return with the current
+ * pressed state.
+ * For event/text based keyboard input, see
+ * RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK.
+ */
+#define RETRO_DEVICE_KEYBOARD 3
+
+/* LIGHTGUN device is similar to Guncon-2 for PlayStation 2.
+ * It reports X/Y coordinates in screen space (similar to the pointer)
+ * in the range [-0x8000, 0x7fff] in both axes, with zero being center and
+ * -0x8000 being out of bounds.
+ * As well as reporting on/off screen state. It features a trigger,
+ * start/select buttons, auxiliary action buttons and a
+ * directional pad. A forced off-screen shot can be requested for
+ * auto-reloading function in some games.
+ */
+#define RETRO_DEVICE_LIGHTGUN 4
+
+/* The ANALOG device is an extension to JOYPAD (RetroPad).
+ * Similar to DualShock2 it adds two analog sticks and all buttons can
+ * be analog. This is treated as a separate device type as it returns
+ * axis values in the full analog range of [-0x7fff, 0x7fff],
+ * although some devices may return -0x8000.
+ * Positive X axis is right. Positive Y axis is down.
+ * Buttons are returned in the range [0, 0x7fff].
+ * Only use ANALOG type when polling for analog values.
+ */
+#define RETRO_DEVICE_ANALOG 5
+
+/* Abstracts the concept of a pointing mechanism, e.g. touch.
+ * This allows libretro to query in absolute coordinates where on the
+ * screen a mouse (or something similar) is being placed.
+ * For a touch centric device, coordinates reported are the coordinates
+ * of the press.
+ *
+ * Coordinates in X and Y are reported as:
+ * [-0x7fff, 0x7fff]: -0x7fff corresponds to the far left/top of the screen,
+ * and 0x7fff corresponds to the far right/bottom of the screen.
+ * The "screen" is here defined as area that is passed to the frontend and
+ * later displayed on the monitor.
+ *
+ * The frontend is free to scale/resize this screen as it sees fit, however,
+ * (X, Y) = (-0x7fff, -0x7fff) will correspond to the top-left pixel of the
+ * game image, etc.
+ *
+ * To check if the pointer coordinates are valid (e.g. a touch display
+ * actually being touched), PRESSED returns 1 or 0.
+ *
+ * If using a mouse on a desktop, PRESSED will usually correspond to the
+ * left mouse button, but this is a frontend decision.
+ * PRESSED will only return 1 if the pointer is inside the game screen.
+ *
+ * For multi-touch, the index variable can be used to successively query
+ * more presses.
+ * If index = 0 returns true for _PRESSED, coordinates can be extracted
+ * with _X, _Y for index = 0. One can then query _PRESSED, _X, _Y with
+ * index = 1, and so on.
+ * Eventually _PRESSED will return false for an index. No further presses
+ * are registered at this point. */
+#define RETRO_DEVICE_POINTER 6
+
+/* Buttons for the RetroPad (JOYPAD).
+ * The placement of these is equivalent to placements on the
+ * Super Nintendo controller.
+ * L2/R2/L3/R3 buttons correspond to the PS1 DualShock.
+ * Also used as id values for RETRO_DEVICE_INDEX_ANALOG_BUTTON */
+#define RETRO_DEVICE_ID_JOYPAD_B 0
+#define RETRO_DEVICE_ID_JOYPAD_Y 1
+#define RETRO_DEVICE_ID_JOYPAD_SELECT 2
+#define RETRO_DEVICE_ID_JOYPAD_START 3
+#define RETRO_DEVICE_ID_JOYPAD_UP 4
+#define RETRO_DEVICE_ID_JOYPAD_DOWN 5
+#define RETRO_DEVICE_ID_JOYPAD_LEFT 6
+#define RETRO_DEVICE_ID_JOYPAD_RIGHT 7
+#define RETRO_DEVICE_ID_JOYPAD_A 8
+#define RETRO_DEVICE_ID_JOYPAD_X 9
+#define RETRO_DEVICE_ID_JOYPAD_L 10
+#define RETRO_DEVICE_ID_JOYPAD_R 11
+#define RETRO_DEVICE_ID_JOYPAD_L2 12
+#define RETRO_DEVICE_ID_JOYPAD_R2 13
+#define RETRO_DEVICE_ID_JOYPAD_L3 14
+#define RETRO_DEVICE_ID_JOYPAD_R3 15
+
+#define RETRO_DEVICE_ID_JOYPAD_MASK 256
+
+/* Index / Id values for ANALOG device. */
+#define RETRO_DEVICE_INDEX_ANALOG_LEFT 0
+#define RETRO_DEVICE_INDEX_ANALOG_RIGHT 1
+#define RETRO_DEVICE_INDEX_ANALOG_BUTTON 2
+#define RETRO_DEVICE_ID_ANALOG_X 0
+#define RETRO_DEVICE_ID_ANALOG_Y 1
+
+/* Id values for MOUSE. */
+#define RETRO_DEVICE_ID_MOUSE_X 0
+#define RETRO_DEVICE_ID_MOUSE_Y 1
+#define RETRO_DEVICE_ID_MOUSE_LEFT 2
+#define RETRO_DEVICE_ID_MOUSE_RIGHT 3
+#define RETRO_DEVICE_ID_MOUSE_WHEELUP 4
+#define RETRO_DEVICE_ID_MOUSE_WHEELDOWN 5
+#define RETRO_DEVICE_ID_MOUSE_MIDDLE 6
+#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP 7
+#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN 8
+#define RETRO_DEVICE_ID_MOUSE_BUTTON_4 9
+#define RETRO_DEVICE_ID_MOUSE_BUTTON_5 10
+
+/* Id values for LIGHTGUN. */
+#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X 13 /*Absolute Position*/
+#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y 14 /*Absolute*/
+#define RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN 15 /*Status Check*/
+#define RETRO_DEVICE_ID_LIGHTGUN_TRIGGER 2
+#define RETRO_DEVICE_ID_LIGHTGUN_RELOAD 16 /*Forced off-screen shot*/
+#define RETRO_DEVICE_ID_LIGHTGUN_AUX_A 3
+#define RETRO_DEVICE_ID_LIGHTGUN_AUX_B 4
+#define RETRO_DEVICE_ID_LIGHTGUN_START 6
+#define RETRO_DEVICE_ID_LIGHTGUN_SELECT 7
+#define RETRO_DEVICE_ID_LIGHTGUN_AUX_C 8
+#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_UP 9
+#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_DOWN 10
+#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_LEFT 11
+#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_RIGHT 12
+/* deprecated */
+#define RETRO_DEVICE_ID_LIGHTGUN_X 0 /*Relative Position*/
+#define RETRO_DEVICE_ID_LIGHTGUN_Y 1 /*Relative*/
+#define RETRO_DEVICE_ID_LIGHTGUN_CURSOR 3 /*Use Aux:A*/
+#define RETRO_DEVICE_ID_LIGHTGUN_TURBO 4 /*Use Aux:B*/
+#define RETRO_DEVICE_ID_LIGHTGUN_PAUSE 5 /*Use Start*/
+
+/* Id values for POINTER. */
+#define RETRO_DEVICE_ID_POINTER_X 0
+#define RETRO_DEVICE_ID_POINTER_Y 1
+#define RETRO_DEVICE_ID_POINTER_PRESSED 2
+#define RETRO_DEVICE_ID_POINTER_COUNT 3
+
+/* Returned from retro_get_region(). */
+#define RETRO_REGION_NTSC 0
+#define RETRO_REGION_PAL 1
+
+/* Id values for LANGUAGE */
+enum retro_language
+{
+ RETRO_LANGUAGE_ENGLISH = 0,
+ RETRO_LANGUAGE_JAPANESE = 1,
+ RETRO_LANGUAGE_FRENCH = 2,
+ RETRO_LANGUAGE_SPANISH = 3,
+ RETRO_LANGUAGE_GERMAN = 4,
+ RETRO_LANGUAGE_ITALIAN = 5,
+ RETRO_LANGUAGE_DUTCH = 6,
+ RETRO_LANGUAGE_PORTUGUESE_BRAZIL = 7,
+ RETRO_LANGUAGE_PORTUGUESE_PORTUGAL = 8,
+ RETRO_LANGUAGE_RUSSIAN = 9,
+ RETRO_LANGUAGE_KOREAN = 10,
+ RETRO_LANGUAGE_CHINESE_TRADITIONAL = 11,
+ RETRO_LANGUAGE_CHINESE_SIMPLIFIED = 12,
+ RETRO_LANGUAGE_ESPERANTO = 13,
+ RETRO_LANGUAGE_POLISH = 14,
+ RETRO_LANGUAGE_VIETNAMESE = 15,
+ RETRO_LANGUAGE_ARABIC = 16,
+ RETRO_LANGUAGE_GREEK = 17,
+ RETRO_LANGUAGE_TURKISH = 18,
+ RETRO_LANGUAGE_SLOVAK = 19,
+ RETRO_LANGUAGE_PERSIAN = 20,
+ RETRO_LANGUAGE_HEBREW = 21,
+ RETRO_LANGUAGE_ASTURIAN = 22,
+ RETRO_LANGUAGE_FINNISH = 23,
+ RETRO_LANGUAGE_INDONESIAN = 24,
+ RETRO_LANGUAGE_SWEDISH = 25,
+ RETRO_LANGUAGE_UKRAINIAN = 26,
+ RETRO_LANGUAGE_CZECH = 27,
+ RETRO_LANGUAGE_CATALAN_VALENCIA = 28,
+ RETRO_LANGUAGE_CATALAN = 29,
+ RETRO_LANGUAGE_BRITISH_ENGLISH = 30,
+ RETRO_LANGUAGE_HUNGARIAN = 31,
+ RETRO_LANGUAGE_LAST,
+
+ /* Ensure sizeof(enum) == sizeof(int) */
+ RETRO_LANGUAGE_DUMMY = INT_MAX
+};
+
+/* Passed to retro_get_memory_data/size().
+ * If the memory type doesn't apply to the
+ * implementation NULL/0 can be returned.
+ */
+#define RETRO_MEMORY_MASK 0xff
+
+/* Regular save RAM. This RAM is usually found on a game cartridge,
+ * backed up by a battery.
+ * If save game data is too complex for a single memory buffer,
+ * the SAVE_DIRECTORY (preferably) or SYSTEM_DIRECTORY environment
+ * callback can be used. */
+#define RETRO_MEMORY_SAVE_RAM 0
+
+/* Some games have a built-in clock to keep track of time.
+ * This memory is usually just a couple of bytes to keep track of time.
+ */
+#define RETRO_MEMORY_RTC 1
+
+/* System ram lets a frontend peek into a game systems main RAM. */
+#define RETRO_MEMORY_SYSTEM_RAM 2
+
+/* Video ram lets a frontend peek into a game systems video RAM (VRAM). */
+#define RETRO_MEMORY_VIDEO_RAM 3
+
+/* Keysyms used for ID in input state callback when polling RETRO_KEYBOARD. */
+enum retro_key
+{
+ RETROK_UNKNOWN = 0,
+ RETROK_FIRST = 0,
+ RETROK_BACKSPACE = 8,
+ RETROK_TAB = 9,
+ RETROK_CLEAR = 12,
+ RETROK_RETURN = 13,
+ RETROK_PAUSE = 19,
+ RETROK_ESCAPE = 27,
+ RETROK_SPACE = 32,
+ RETROK_EXCLAIM = 33,
+ RETROK_QUOTEDBL = 34,
+ RETROK_HASH = 35,
+ RETROK_DOLLAR = 36,
+ RETROK_AMPERSAND = 38,
+ RETROK_QUOTE = 39,
+ RETROK_LEFTPAREN = 40,
+ RETROK_RIGHTPAREN = 41,
+ RETROK_ASTERISK = 42,
+ RETROK_PLUS = 43,
+ RETROK_COMMA = 44,
+ RETROK_MINUS = 45,
+ RETROK_PERIOD = 46,
+ RETROK_SLASH = 47,
+ RETROK_0 = 48,
+ RETROK_1 = 49,
+ RETROK_2 = 50,
+ RETROK_3 = 51,
+ RETROK_4 = 52,
+ RETROK_5 = 53,
+ RETROK_6 = 54,
+ RETROK_7 = 55,
+ RETROK_8 = 56,
+ RETROK_9 = 57,
+ RETROK_COLON = 58,
+ RETROK_SEMICOLON = 59,
+ RETROK_LESS = 60,
+ RETROK_EQUALS = 61,
+ RETROK_GREATER = 62,
+ RETROK_QUESTION = 63,
+ RETROK_AT = 64,
+ RETROK_LEFTBRACKET = 91,
+ RETROK_BACKSLASH = 92,
+ RETROK_RIGHTBRACKET = 93,
+ RETROK_CARET = 94,
+ RETROK_UNDERSCORE = 95,
+ RETROK_BACKQUOTE = 96,
+ RETROK_a = 97,
+ RETROK_b = 98,
+ RETROK_c = 99,
+ RETROK_d = 100,
+ RETROK_e = 101,
+ RETROK_f = 102,
+ RETROK_g = 103,
+ RETROK_h = 104,
+ RETROK_i = 105,
+ RETROK_j = 106,
+ RETROK_k = 107,
+ RETROK_l = 108,
+ RETROK_m = 109,
+ RETROK_n = 110,
+ RETROK_o = 111,
+ RETROK_p = 112,
+ RETROK_q = 113,
+ RETROK_r = 114,
+ RETROK_s = 115,
+ RETROK_t = 116,
+ RETROK_u = 117,
+ RETROK_v = 118,
+ RETROK_w = 119,
+ RETROK_x = 120,
+ RETROK_y = 121,
+ RETROK_z = 122,
+ RETROK_LEFTBRACE = 123,
+ RETROK_BAR = 124,
+ RETROK_RIGHTBRACE = 125,
+ RETROK_TILDE = 126,
+ RETROK_DELETE = 127,
+
+ RETROK_KP0 = 256,
+ RETROK_KP1 = 257,
+ RETROK_KP2 = 258,
+ RETROK_KP3 = 259,
+ RETROK_KP4 = 260,
+ RETROK_KP5 = 261,
+ RETROK_KP6 = 262,
+ RETROK_KP7 = 263,
+ RETROK_KP8 = 264,
+ RETROK_KP9 = 265,
+ RETROK_KP_PERIOD = 266,
+ RETROK_KP_DIVIDE = 267,
+ RETROK_KP_MULTIPLY = 268,
+ RETROK_KP_MINUS = 269,
+ RETROK_KP_PLUS = 270,
+ RETROK_KP_ENTER = 271,
+ RETROK_KP_EQUALS = 272,
+
+ RETROK_UP = 273,
+ RETROK_DOWN = 274,
+ RETROK_RIGHT = 275,
+ RETROK_LEFT = 276,
+ RETROK_INSERT = 277,
+ RETROK_HOME = 278,
+ RETROK_END = 279,
+ RETROK_PAGEUP = 280,
+ RETROK_PAGEDOWN = 281,
+
+ RETROK_F1 = 282,
+ RETROK_F2 = 283,
+ RETROK_F3 = 284,
+ RETROK_F4 = 285,
+ RETROK_F5 = 286,
+ RETROK_F6 = 287,
+ RETROK_F7 = 288,
+ RETROK_F8 = 289,
+ RETROK_F9 = 290,
+ RETROK_F10 = 291,
+ RETROK_F11 = 292,
+ RETROK_F12 = 293,
+ RETROK_F13 = 294,
+ RETROK_F14 = 295,
+ RETROK_F15 = 296,
+
+ RETROK_NUMLOCK = 300,
+ RETROK_CAPSLOCK = 301,
+ RETROK_SCROLLOCK = 302,
+ RETROK_RSHIFT = 303,
+ RETROK_LSHIFT = 304,
+ RETROK_RCTRL = 305,
+ RETROK_LCTRL = 306,
+ RETROK_RALT = 307,
+ RETROK_LALT = 308,
+ RETROK_RMETA = 309,
+ RETROK_LMETA = 310,
+ RETROK_LSUPER = 311,
+ RETROK_RSUPER = 312,
+ RETROK_MODE = 313,
+ RETROK_COMPOSE = 314,
+
+ RETROK_HELP = 315,
+ RETROK_PRINT = 316,
+ RETROK_SYSREQ = 317,
+ RETROK_BREAK = 318,
+ RETROK_MENU = 319,
+ RETROK_POWER = 320,
+ RETROK_EURO = 321,
+ RETROK_UNDO = 322,
+ RETROK_OEM_102 = 323,
+
+ RETROK_LAST,
+
+ RETROK_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
+};
+
+enum retro_mod
+{
+ RETROKMOD_NONE = 0x0000,
+
+ RETROKMOD_SHIFT = 0x01,
+ RETROKMOD_CTRL = 0x02,
+ RETROKMOD_ALT = 0x04,
+ RETROKMOD_META = 0x08,
+
+ RETROKMOD_NUMLOCK = 0x10,
+ RETROKMOD_CAPSLOCK = 0x20,
+ RETROKMOD_SCROLLOCK = 0x40,
+
+ RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
+};
+
+/* If set, this call is not part of the public libretro API yet. It can
+ * change or be removed at any time. */
+#define RETRO_ENVIRONMENT_EXPERIMENTAL 0x10000
+/* Environment callback to be used internally in frontend. */
+#define RETRO_ENVIRONMENT_PRIVATE 0x20000
+
+/* Environment commands. */
+#define RETRO_ENVIRONMENT_SET_ROTATION 1 /* const unsigned * --
+ * Sets screen rotation of graphics.
+ * Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180,
+ * 270 degrees counter-clockwise respectively.
+ */
+#define RETRO_ENVIRONMENT_GET_OVERSCAN 2 /* bool * --
+ * NOTE: As of 2019 this callback is considered deprecated in favor of
+ * using core options to manage overscan in a more nuanced, core-specific way.
+ *
+ * Boolean value whether or not the implementation should use overscan,
+ * or crop away overscan.
+ */
+#define RETRO_ENVIRONMENT_GET_CAN_DUPE 3 /* bool * --
+ * Boolean value whether or not frontend supports frame duping,
+ * passing NULL to video frame callback.
+ */
+
+ /* Environ 4, 5 are no longer supported (GET_VARIABLE / SET_VARIABLES),
+ * and reserved to avoid possible ABI clash.
+ */
+
+#define RETRO_ENVIRONMENT_SET_MESSAGE 6 /* const struct retro_message * --
+ * Sets a message to be displayed in implementation-specific manner
+ * for a certain amount of 'frames'.
+ * Should not be used for trivial messages, which should simply be
+ * logged via RETRO_ENVIRONMENT_GET_LOG_INTERFACE (or as a
+ * fallback, stderr).
+ */
+#define RETRO_ENVIRONMENT_SHUTDOWN 7 /* N/A (NULL) --
+ * Requests the frontend to shutdown.
+ * Should only be used if game has a specific
+ * way to shutdown the game from a menu item or similar.
+ */
+#define RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL 8
+ /* const unsigned * --
+ * Gives a hint to the frontend how demanding this implementation
+ * is on a system. E.g. reporting a level of 2 means
+ * this implementation should run decently on all frontends
+ * of level 2 and up.
+ *
+ * It can be used by the frontend to potentially warn
+ * about too demanding implementations.
+ *
+ * The levels are "floating".
+ *
+ * This function can be called on a per-game basis,
+ * as certain games an implementation can play might be
+ * particularly demanding.
+ * If called, it should be called in retro_load_game().
+ */
+#define RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY 9
+ /* const char ** --
+ * Returns the "system" directory of the frontend.
+ * This directory can be used to store system specific
+ * content such as BIOSes, configuration data, etc.
+ * The returned value can be NULL.
+ * If so, no such directory is defined,
+ * and it's up to the implementation to find a suitable directory.
+ *
+ * NOTE: Some cores used this folder also for "save" data such as
+ * memory cards, etc, for lack of a better place to put it.
+ * This is now discouraged, and if possible, cores should try to
+ * use the new GET_SAVE_DIRECTORY.
+ */
+#define RETRO_ENVIRONMENT_SET_PIXEL_FORMAT 10
+ /* const enum retro_pixel_format * --
+ * Sets the internal pixel format used by the implementation.
+ * The default pixel format is RETRO_PIXEL_FORMAT_0RGB1555.
+ * This pixel format however, is deprecated (see enum retro_pixel_format).
+ * If the call returns false, the frontend does not support this pixel
+ * format.
+ *
+ * This function should be called inside retro_load_game() or
+ * retro_get_system_av_info().
+ */
+#define RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS 11
+ /* const struct retro_input_descriptor * --
+ * Sets an array of retro_input_descriptors.
+ * It is up to the frontend to present this in a usable way.
+ * The array is terminated by retro_input_descriptor::description
+ * being set to NULL.
+ * This function can be called at any time, but it is recommended
+ * to call it as early as possible.
+ */
+#define RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK 12
+ /* const struct retro_keyboard_callback * --
+ * Sets a callback function used to notify core about keyboard events.
+ */
+#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE 13
+ /* const struct retro_disk_control_callback * --
+ * Sets an interface which frontend can use to eject and insert
+ * disk images.
+ * This is used for games which consist of multiple images and
+ * must be manually swapped out by the user (e.g. PSX).
+ */
+#define RETRO_ENVIRONMENT_SET_HW_RENDER 14
+ /* struct retro_hw_render_callback * --
+ * Sets an interface to let a libretro core render with
+ * hardware acceleration.
+ * Should be called in retro_load_game().
+ * If successful, libretro cores will be able to render to a
+ * frontend-provided framebuffer.
+ * The size of this framebuffer will be at least as large as
+ * max_width/max_height provided in get_av_info().
+ * If HW rendering is used, pass only RETRO_HW_FRAME_BUFFER_VALID or
+ * NULL to retro_video_refresh_t.
+ */
+#define RETRO_ENVIRONMENT_GET_VARIABLE 15
+ /* struct retro_variable * --
+ * Interface to acquire user-defined information from environment
+ * that cannot feasibly be supported in a multi-system way.
+ * 'key' should be set to a key which has already been set by
+ * SET_VARIABLES.
+ * 'data' will be set to a value or NULL.
+ */
+#define RETRO_ENVIRONMENT_SET_VARIABLES 16
+ /* const struct retro_variable * --
+ * Allows an implementation to signal the environment
+ * which variables it might want to check for later using
+ * GET_VARIABLE.
+ * This allows the frontend to present these variables to
+ * a user dynamically.
+ * This should be called the first time as early as
+ * possible (ideally in retro_set_environment).
+ * Afterward it may be called again for the core to communicate
+ * updated options to the frontend, but the number of core
+ * options must not change from the number in the initial call.
+ *
+ * 'data' points to an array of retro_variable structs
+ * terminated by a { NULL, NULL } element.
+ * retro_variable::key should be namespaced to not collide
+ * with other implementations' keys. E.g. A core called
+ * 'foo' should use keys named as 'foo_option'.
+ * retro_variable::value should contain a human readable
+ * description of the key as well as a '|' delimited list
+ * of expected values.
+ *
+ * The number of possible options should be very limited,
+ * i.e. it should be feasible to cycle through options
+ * without a keyboard.
+ *
+ * First entry should be treated as a default.
+ *
+ * Example entry:
+ * { "foo_option", "Speed hack coprocessor X; false|true" }
+ *
+ * Text before first ';' is description. This ';' must be
+ * followed by a space, and followed by a list of possible
+ * values split up with '|'.
+ *
+ * Only strings are operated on. The possible values will
+ * generally be displayed and stored as-is by the frontend.
+ */
+#define RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE 17
+ /* bool * --
+ * Result is set to true if some variables are updated by
+ * frontend since last call to RETRO_ENVIRONMENT_GET_VARIABLE.
+ * Variables should be queried with GET_VARIABLE.
+ */
+#define RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME 18
+ /* const bool * --
+ * If true, the libretro implementation supports calls to
+ * retro_load_game() with NULL as argument.
+ * Used by cores which can run without particular game data.
+ * This should be called within retro_set_environment() only.
+ */
+#define RETRO_ENVIRONMENT_GET_LIBRETRO_PATH 19
+ /* const char ** --
+ * Retrieves the absolute path from where this libretro
+ * implementation was loaded.
+ * NULL is returned if the libretro was loaded statically
+ * (i.e. linked statically to frontend), or if the path cannot be
+ * determined.
+ * Mostly useful in cooperation with SET_SUPPORT_NO_GAME as assets can
+ * be loaded without ugly hacks.
+ */
+
+ /* Environment 20 was an obsolete version of SET_AUDIO_CALLBACK.
+ * It was not used by any known core at the time,
+ * and was removed from the API. */
+#define RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK 21
+ /* const struct retro_frame_time_callback * --
+ * Lets the core know how much time has passed since last
+ * invocation of retro_run().
+ * The frontend can tamper with the timing to fake fast-forward,
+ * slow-motion, frame stepping, etc.
+ * In this case the delta time will use the reference value
+ * in frame_time_callback..
+ */
+#define RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK 22
+ /* const struct retro_audio_callback * --
+ * Sets an interface which is used to notify a libretro core about audio
+ * being available for writing.
+ * The callback can be called from any thread, so a core using this must
+ * have a thread safe audio implementation.
+ * It is intended for games where audio and video are completely
+ * asynchronous and audio can be generated on the fly.
+ * This interface is not recommended for use with emulators which have
+ * highly synchronous audio.
+ *
+ * The callback only notifies about writability; the libretro core still
+ * has to call the normal audio callbacks
+ * to write audio. The audio callbacks must be called from within the
+ * notification callback.
+ * The amount of audio data to write is up to the implementation.
+ * Generally, the audio callback will be called continously in a loop.
+ *
+ * Due to thread safety guarantees and lack of sync between audio and
+ * video, a frontend can selectively disallow this interface based on
+ * internal configuration. A core using this interface must also
+ * implement the "normal" audio interface.
+ *
+ * A libretro core using SET_AUDIO_CALLBACK should also make use of
+ * SET_FRAME_TIME_CALLBACK.
+ */
+#define RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE 23
+ /* struct retro_rumble_interface * --
+ * Gets an interface which is used by a libretro core to set
+ * state of rumble motors in controllers.
+ * A strong and weak motor is supported, and they can be
+ * controlled indepedently.
+ * Should be called from either retro_init() or retro_load_game().
+ * Should not be called from retro_set_environment().
+ * Returns false if rumble functionality is unavailable.
+ */
+#define RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES 24
+ /* uint64_t * --
+ * Gets a bitmask telling which device type are expected to be
+ * handled properly in a call to retro_input_state_t.
+ * Devices which are not handled or recognized always return
+ * 0 in retro_input_state_t.
+ * Example bitmask: caps = (1 << RETRO_DEVICE_JOYPAD) | (1 << RETRO_DEVICE_ANALOG).
+ * Should only be called in retro_run().
+ */
+#define RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE (25 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_sensor_interface * --
+ * Gets access to the sensor interface.
+ * The purpose of this interface is to allow
+ * setting state related to sensors such as polling rate,
+ * enabling/disable it entirely, etc.
+ * Reading sensor state is done via the normal
+ * input_state_callback API.
+ */
+#define RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE (26 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_camera_callback * --
+ * Gets an interface to a video camera driver.
+ * A libretro core can use this interface to get access to a
+ * video camera.
+ * New video frames are delivered in a callback in same
+ * thread as retro_run().
+ *
+ * GET_CAMERA_INTERFACE should be called in retro_load_game().
+ *
+ * Depending on the camera implementation used, camera frames
+ * will be delivered as a raw framebuffer,
+ * or as an OpenGL texture directly.
+ *
+ * The core has to tell the frontend here which types of
+ * buffers can be handled properly.
+ * An OpenGL texture can only be handled when using a
+ * libretro GL core (SET_HW_RENDER).
+ * It is recommended to use a libretro GL core when
+ * using camera interface.
+ *
+ * The camera is not started automatically. The retrieved start/stop
+ * functions must be used to explicitly
+ * start and stop the camera driver.
+ */
+#define RETRO_ENVIRONMENT_GET_LOG_INTERFACE 27
+ /* struct retro_log_callback * --
+ * Gets an interface for logging. This is useful for
+ * logging in a cross-platform way
+ * as certain platforms cannot use stderr for logging.
+ * It also allows the frontend to
+ * show logging information in a more suitable way.
+ * If this interface is not used, libretro cores should
+ * log to stderr as desired.
+ */
+#define RETRO_ENVIRONMENT_GET_PERF_INTERFACE 28
+ /* struct retro_perf_callback * --
+ * Gets an interface for performance counters. This is useful
+ * for performance logging in a cross-platform way and for detecting
+ * architecture-specific features, such as SIMD support.
+ */
+#define RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE 29
+ /* struct retro_location_callback * --
+ * Gets access to the location interface.
+ * The purpose of this interface is to be able to retrieve
+ * location-based information from the host device,
+ * such as current latitude / longitude.
+ */
+#define RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY 30 /* Old name, kept for compatibility. */
+#define RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY 30
+ /* const char ** --
+ * Returns the "core assets" directory of the frontend.
+ * This directory can be used to store specific assets that the
+ * core relies upon, such as art assets,
+ * input data, etc etc.
+ * The returned value can be NULL.
+ * If so, no such directory is defined,
+ * and it's up to the implementation to find a suitable directory.
+ */
+#define RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY 31
+ /* const char ** --
+ * Returns the "save" directory of the frontend, unless there is no
+ * save directory available. The save directory should be used to
+ * store SRAM, memory cards, high scores, etc, if the libretro core
+ * cannot use the regular memory interface (retro_get_memory_data()).
+ *
+ * If the frontend cannot designate a save directory, it will return
+ * NULL to indicate that the core should attempt to operate without a
+ * save directory set.
+ *
+ * NOTE: early libretro cores used the system directory for save
+ * files. Cores that need to be backwards-compatible can still check
+ * GET_SYSTEM_DIRECTORY.
+ */
+#define RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO 32
+ /* const struct retro_system_av_info * --
+ * Sets a new av_info structure. This can only be called from
+ * within retro_run().
+ * This should *only* be used if the core is completely altering the
+ * internal resolutions, aspect ratios, timings, sampling rate, etc.
+ * Calling this can require a full reinitialization of video/audio
+ * drivers in the frontend,
+ *
+ * so it is important to call it very sparingly, and usually only with
+ * the users explicit consent.
+ * An eventual driver reinitialize will happen so that video and
+ * audio callbacks
+ * happening after this call within the same retro_run() call will
+ * target the newly initialized driver.
+ *
+ * This callback makes it possible to support configurable resolutions
+ * in games, which can be useful to
+ * avoid setting the "worst case" in max_width/max_height.
+ *
+ * ***HIGHLY RECOMMENDED*** Do not call this callback every time
+ * resolution changes in an emulator core if it's
+ * expected to be a temporary change, for the reasons of possible
+ * driver reinitialization.
+ * This call is not a free pass for not trying to provide
+ * correct values in retro_get_system_av_info(). If you need to change
+ * things like aspect ratio or nominal width/height,
+ * use RETRO_ENVIRONMENT_SET_GEOMETRY, which is a softer variant
+ * of SET_SYSTEM_AV_INFO.
+ *
+ * If this returns false, the frontend does not acknowledge a
+ * changed av_info struct.
+ */
+#define RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK 33
+ /* const struct retro_get_proc_address_interface * --
+ * Allows a libretro core to announce support for the
+ * get_proc_address() interface.
+ * This interface allows for a standard way to extend libretro where
+ * use of environment calls are too indirect,
+ * e.g. for cases where the frontend wants to call directly into the core.
+ *
+ * If a core wants to expose this interface, SET_PROC_ADDRESS_CALLBACK
+ * **MUST** be called from within retro_set_environment().
+ */
+#define RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO 34
+ /* const struct retro_subsystem_info * --
+ * This environment call introduces the concept of libretro "subsystems".
+ * A subsystem is a variant of a libretro core which supports
+ * different kinds of games.
+ * The purpose of this is to support e.g. emulators which might
+ * have special needs, e.g. Super Nintendo's Super GameBoy, Sufami Turbo.
+ * It can also be used to pick among subsystems in an explicit way
+ * if the libretro implementation is a multi-system emulator itself.
+ *
+ * Loading a game via a subsystem is done with retro_load_game_special(),
+ * and this environment call allows a libretro core to expose which
+ * subsystems are supported for use with retro_load_game_special().
+ * A core passes an array of retro_game_special_info which is terminated
+ * with a zeroed out retro_game_special_info struct.
+ *
+ * If a core wants to use this functionality, SET_SUBSYSTEM_INFO
+ * **MUST** be called from within retro_set_environment().
+ */
+#define RETRO_ENVIRONMENT_SET_CONTROLLER_INFO 35
+ /* const struct retro_controller_info * --
+ * This environment call lets a libretro core tell the frontend
+ * which controller subclasses are recognized in calls to
+ * retro_set_controller_port_device().
+ *
+ * Some emulators such as Super Nintendo support multiple lightgun
+ * types which must be specifically selected from. It is therefore
+ * sometimes necessary for a frontend to be able to tell the core
+ * about a special kind of input device which is not specifcally
+ * provided by the Libretro API.
+ *
+ * In order for a frontend to understand the workings of those devices,
+ * they must be defined as a specialized subclass of the generic device
+ * types already defined in the libretro API.
+ *
+ * The core must pass an array of const struct retro_controller_info which
+ * is terminated with a blanked out struct. Each element of the
+ * retro_controller_info struct corresponds to the ascending port index
+ * that is passed to retro_set_controller_port_device() when that function
+ * is called to indicate to the core that the frontend has changed the
+ * active device subclass. SEE ALSO: retro_set_controller_port_device()
+ *
+ * The ascending input port indexes provided by the core in the struct
+ * are generally presented by frontends as ascending User # or Player #,
+ * such as Player 1, Player 2, Player 3, etc. Which device subclasses are
+ * supported can vary per input port.
+ *
+ * The first inner element of each entry in the retro_controller_info array
+ * is a retro_controller_description struct that specifies the names and
+ * codes of all device subclasses that are available for the corresponding
+ * User or Player, beginning with the generic Libretro device that the
+ * subclasses are derived from. The second inner element of each entry is the
+ * total number of subclasses that are listed in the retro_controller_description.
+ *
+ * NOTE: Even if special device types are set in the libretro core,
+ * libretro should only poll input based on the base input device types.
+ */
+#define RETRO_ENVIRONMENT_SET_MEMORY_MAPS (36 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* const struct retro_memory_map * --
+ * This environment call lets a libretro core tell the frontend
+ * about the memory maps this core emulates.
+ * This can be used to implement, for example, cheats in a core-agnostic way.
+ *
+ * Should only be used by emulators; it doesn't make much sense for
+ * anything else.
+ * It is recommended to expose all relevant pointers through
+ * retro_get_memory_* as well.
+ *
+ * Can be called from retro_init and retro_load_game.
+ */
+#define RETRO_ENVIRONMENT_SET_GEOMETRY 37
+ /* const struct retro_game_geometry * --
+ * This environment call is similar to SET_SYSTEM_AV_INFO for changing
+ * video parameters, but provides a guarantee that drivers will not be
+ * reinitialized.
+ * This can only be called from within retro_run().
+ *
+ * The purpose of this call is to allow a core to alter nominal
+ * width/heights as well as aspect ratios on-the-fly, which can be
+ * useful for some emulators to change in run-time.
+ *
+ * max_width/max_height arguments are ignored and cannot be changed
+ * with this call as this could potentially require a reinitialization or a
+ * non-constant time operation.
+ * If max_width/max_height are to be changed, SET_SYSTEM_AV_INFO is required.
+ *
+ * A frontend must guarantee that this environment call completes in
+ * constant time.
+ */
+#define RETRO_ENVIRONMENT_GET_USERNAME 38
+ /* const char **
+ * Returns the specified username of the frontend, if specified by the user.
+ * This username can be used as a nickname for a core that has online facilities
+ * or any other mode where personalization of the user is desirable.
+ * The returned value can be NULL.
+ * If this environ callback is used by a core that requires a valid username,
+ * a default username should be specified by the core.
+ */
+#define RETRO_ENVIRONMENT_GET_LANGUAGE 39
+ /* unsigned * --
+ * Returns the specified language of the frontend, if specified by the user.
+ * It can be used by the core for localization purposes.
+ */
+#define RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER (40 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_framebuffer * --
+ * Returns a preallocated framebuffer which the core can use for rendering
+ * the frame into when not using SET_HW_RENDER.
+ * The framebuffer returned from this call must not be used
+ * after the current call to retro_run() returns.
+ *
+ * The goal of this call is to allow zero-copy behavior where a core
+ * can render directly into video memory, avoiding extra bandwidth cost by copying
+ * memory from core to video memory.
+ *
+ * If this call succeeds and the core renders into it,
+ * the framebuffer pointer and pitch can be passed to retro_video_refresh_t.
+ * If the buffer from GET_CURRENT_SOFTWARE_FRAMEBUFFER is to be used,
+ * the core must pass the exact
+ * same pointer as returned by GET_CURRENT_SOFTWARE_FRAMEBUFFER;
+ * i.e. passing a pointer which is offset from the
+ * buffer is undefined. The width, height and pitch parameters
+ * must also match exactly to the values obtained from GET_CURRENT_SOFTWARE_FRAMEBUFFER.
+ *
+ * It is possible for a frontend to return a different pixel format
+ * than the one used in SET_PIXEL_FORMAT. This can happen if the frontend
+ * needs to perform conversion.
+ *
+ * It is still valid for a core to render to a different buffer
+ * even if GET_CURRENT_SOFTWARE_FRAMEBUFFER succeeds.
+ *
+ * A frontend must make sure that the pointer obtained from this function is
+ * writeable (and readable).
+ */
+#define RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE (41 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* const struct retro_hw_render_interface ** --
+ * Returns an API specific rendering interface for accessing API specific data.
+ * Not all HW rendering APIs support or need this.
+ * The contents of the returned pointer is specific to the rendering API
+ * being used. See the various headers like libretro_vulkan.h, etc.
+ *
+ * GET_HW_RENDER_INTERFACE cannot be called before context_reset has been called.
+ * Similarly, after context_destroyed callback returns,
+ * the contents of the HW_RENDER_INTERFACE are invalidated.
+ */
+#define RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS (42 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* const bool * --
+ * If true, the libretro implementation supports achievements
+ * either via memory descriptors set with RETRO_ENVIRONMENT_SET_MEMORY_MAPS
+ * or via retro_get_memory_data/retro_get_memory_size.
+ *
+ * This must be called before the first call to retro_run.
+ */
+#define RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE (43 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* const struct retro_hw_render_context_negotiation_interface * --
+ * Sets an interface which lets the libretro core negotiate with frontend how a context is created.
+ * The semantics of this interface depends on which API is used in SET_HW_RENDER earlier.
+ * This interface will be used when the frontend is trying to create a HW rendering context,
+ * so it will be used after SET_HW_RENDER, but before the context_reset callback.
+ */
+#define RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS 44
+ /* uint64_t * --
+ * Sets quirk flags associated with serialization. The frontend will zero any flags it doesn't
+ * recognize or support. Should be set in either retro_init or retro_load_game, but not both.
+ */
+#define RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT (44 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* N/A (null) * --
+ * The frontend will try to use a 'shared' hardware context (mostly applicable
+ * to OpenGL) when a hardware context is being set up.
+ *
+ * Returns true if the frontend supports shared hardware contexts and false
+ * if the frontend does not support shared hardware contexts.
+ *
+ * This will do nothing on its own until SET_HW_RENDER env callbacks are
+ * being used.
+ */
+#define RETRO_ENVIRONMENT_GET_VFS_INTERFACE (45 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_vfs_interface_info * --
+ * Gets access to the VFS interface.
+ * VFS presence needs to be queried prior to load_game or any
+ * get_system/save/other_directory being called to let front end know
+ * core supports VFS before it starts handing out paths.
+ * It is recomended to do so in retro_set_environment
+ */
+#define RETRO_ENVIRONMENT_GET_LED_INTERFACE (46 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_led_interface * --
+ * Gets an interface which is used by a libretro core to set
+ * state of LEDs.
+ */
+#define RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE (47 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* int * --
+ * Tells the core if the frontend wants audio or video.
+ * If disabled, the frontend will discard the audio or video,
+ * so the core may decide to skip generating a frame or generating audio.
+ * This is mainly used for increasing performance.
+ * Bit 0 (value 1): Enable Video
+ * Bit 1 (value 2): Enable Audio
+ * Bit 2 (value 4): Use Fast Savestates.
+ * Bit 3 (value 8): Hard Disable Audio
+ * Other bits are reserved for future use and will default to zero.
+ * If video is disabled:
+ * * The frontend wants the core to not generate any video,
+ * including presenting frames via hardware acceleration.
+ * * The frontend's video frame callback will do nothing.
+ * * After running the frame, the video output of the next frame should be
+ * no different than if video was enabled, and saving and loading state
+ * should have no issues.
+ * If audio is disabled:
+ * * The frontend wants the core to not generate any audio.
+ * * The frontend's audio callbacks will do nothing.
+ * * After running the frame, the audio output of the next frame should be
+ * no different than if audio was enabled, and saving and loading state
+ * should have no issues.
+ * Fast Savestates:
+ * * Guaranteed to be created by the same binary that will load them.
+ * * Will not be written to or read from the disk.
+ * * Suggest that the core assumes loading state will succeed.
+ * * Suggest that the core updates its memory buffers in-place if possible.
+ * * Suggest that the core skips clearing memory.
+ * * Suggest that the core skips resetting the system.
+ * * Suggest that the core may skip validation steps.
+ * Hard Disable Audio:
+ * * Used for a secondary core when running ahead.
+ * * Indicates that the frontend will never need audio from the core.
+ * * Suggests that the core may stop synthesizing audio, but this should not
+ * compromise emulation accuracy.
+ * * Audio output for the next frame does not matter, and the frontend will
+ * never need an accurate audio state in the future.
+ * * State will never be saved when using Hard Disable Audio.
+ */
+#define RETRO_ENVIRONMENT_GET_MIDI_INTERFACE (48 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_midi_interface ** --
+ * Returns a MIDI interface that can be used for raw data I/O.
+ */
+
+#define RETRO_ENVIRONMENT_GET_FASTFORWARDING (49 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* bool * --
+ * Boolean value that indicates whether or not the frontend is in
+ * fastforwarding mode.
+ */
+
+#define RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE (50 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* float * --
+ * Float value that lets us know what target refresh rate
+ * is curently in use by the frontend.
+ *
+ * The core can use the returned value to set an ideal
+ * refresh rate/framerate.
+ */
+
+#define RETRO_ENVIRONMENT_GET_INPUT_BITMASKS (51 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* bool * --
+ * Boolean value that indicates whether or not the frontend supports
+ * input bitmasks being returned by retro_input_state_t. The advantage
+ * of this is that retro_input_state_t has to be only called once to
+ * grab all button states instead of multiple times.
+ *
+ * If it returns true, you can pass RETRO_DEVICE_ID_JOYPAD_MASK as 'id'
+ * to retro_input_state_t (make sure 'device' is set to RETRO_DEVICE_JOYPAD).
+ * It will return a bitmask of all the digital buttons.
+ */
+
+#define RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION 52
+ /* unsigned * --
+ * Unsigned value is the API version number of the core options
+ * interface supported by the frontend. If callback return false,
+ * API version is assumed to be 0.
+ *
+ * In legacy code, core options are set by passing an array of
+ * retro_variable structs to RETRO_ENVIRONMENT_SET_VARIABLES.
+ * This may be still be done regardless of the core options
+ * interface version.
+ *
+ * If version is >= 1 however, core options may instead be set by
+ * passing an array of retro_core_option_definition structs to
+ * RETRO_ENVIRONMENT_SET_CORE_OPTIONS, or a 2D array of
+ * retro_core_option_definition structs to RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL.
+ * This allows the core to additionally set option sublabel information
+ * and/or provide localisation support.
+ *
+ * If version is >= 2, core options may instead be set by passing
+ * a retro_core_options_v2 struct to RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2,
+ * or an array of retro_core_options_v2 structs to
+ * RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL. This allows the core
+ * to additionally set optional core option category information
+ * for frontends with core option category support.
+ */
+
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS 53
+ /* const struct retro_core_option_definition ** --
+ * Allows an implementation to signal the environment
+ * which variables it might want to check for later using
+ * GET_VARIABLE.
+ * This allows the frontend to present these variables to
+ * a user dynamically.
+ * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION
+ * returns an API version of >= 1.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES.
+ * This should be called the first time as early as
+ * possible (ideally in retro_set_environment).
+ * Afterwards it may be called again for the core to communicate
+ * updated options to the frontend, but the number of core
+ * options must not change from the number in the initial call.
+ *
+ * 'data' points to an array of retro_core_option_definition structs
+ * terminated by a { NULL, NULL, NULL, {{0}}, NULL } element.
+ * retro_core_option_definition::key should be namespaced to not collide
+ * with other implementations' keys. e.g. A core called
+ * 'foo' should use keys named as 'foo_option'.
+ * retro_core_option_definition::desc should contain a human readable
+ * description of the key.
+ * retro_core_option_definition::info should contain any additional human
+ * readable information text that a typical user may need to
+ * understand the functionality of the option.
+ * retro_core_option_definition::values is an array of retro_core_option_value
+ * structs terminated by a { NULL, NULL } element.
+ * > retro_core_option_definition::values[index].value is an expected option
+ * value.
+ * > retro_core_option_definition::values[index].label is a human readable
+ * label used when displaying the value on screen. If NULL,
+ * the value itself is used.
+ * retro_core_option_definition::default_value is the default core option
+ * setting. It must match one of the expected option values in the
+ * retro_core_option_definition::values array. If it does not, or the
+ * default value is NULL, the first entry in the
+ * retro_core_option_definition::values array is treated as the default.
+ *
+ * The number of possible option values should be very limited,
+ * and must be less than RETRO_NUM_CORE_OPTION_VALUES_MAX.
+ * i.e. it should be feasible to cycle through options
+ * without a keyboard.
+ *
+ * Example entry:
+ * {
+ * "foo_option",
+ * "Speed hack coprocessor X",
+ * "Provides increased performance at the expense of reduced accuracy",
+ * {
+ * { "false", NULL },
+ * { "true", NULL },
+ * { "unstable", "Turbo (Unstable)" },
+ * { NULL, NULL },
+ * },
+ * "false"
+ * }
+ *
+ * Only strings are operated on. The possible values will
+ * generally be displayed and stored as-is by the frontend.
+ */
+
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL 54
+ /* const struct retro_core_options_intl * --
+ * Allows an implementation to signal the environment
+ * which variables it might want to check for later using
+ * GET_VARIABLE.
+ * This allows the frontend to present these variables to
+ * a user dynamically.
+ * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION
+ * returns an API version of >= 1.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS.
+ * This should be called the first time as early as
+ * possible (ideally in retro_set_environment).
+ * Afterwards it may be called again for the core to communicate
+ * updated options to the frontend, but the number of core
+ * options must not change from the number in the initial call.
+ *
+ * This is fundamentally the same as RETRO_ENVIRONMENT_SET_CORE_OPTIONS,
+ * with the addition of localisation support. The description of the
+ * RETRO_ENVIRONMENT_SET_CORE_OPTIONS callback should be consulted
+ * for further details.
+ *
+ * 'data' points to a retro_core_options_intl struct.
+ *
+ * retro_core_options_intl::us is a pointer to an array of
+ * retro_core_option_definition structs defining the US English
+ * core options implementation. It must point to a valid array.
+ *
+ * retro_core_options_intl::local is a pointer to an array of
+ * retro_core_option_definition structs defining core options for
+ * the current frontend language. It may be NULL (in which case
+ * retro_core_options_intl::us is used by the frontend). Any items
+ * missing from this array will be read from retro_core_options_intl::us
+ * instead.
+ *
+ * NOTE: Default core option values are always taken from the
+ * retro_core_options_intl::us array. Any default values in
+ * retro_core_options_intl::local array will be ignored.
+ */
+
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY 55
+ /* struct retro_core_option_display * --
+ *
+ * Allows an implementation to signal the environment to show
+ * or hide a variable when displaying core options. This is
+ * considered a *suggestion*. The frontend is free to ignore
+ * this callback, and its implementation not considered mandatory.
+ *
+ * 'data' points to a retro_core_option_display struct
+ *
+ * retro_core_option_display::key is a variable identifier
+ * which has already been set by SET_VARIABLES/SET_CORE_OPTIONS.
+ *
+ * retro_core_option_display::visible is a boolean, specifying
+ * whether variable should be displayed
+ *
+ * Note that all core option variables will be set visible by
+ * default when calling SET_VARIABLES/SET_CORE_OPTIONS.
+ */
+
+#define RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER 56
+ /* unsigned * --
+ *
+ * Allows an implementation to ask frontend preferred hardware
+ * context to use. Core should use this information to deal
+ * with what specific context to request with SET_HW_RENDER.
+ *
+ * 'data' points to an unsigned variable
+ */
+
+#define RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION 57
+ /* unsigned * --
+ * Unsigned value is the API version number of the disk control
+ * interface supported by the frontend. If callback return false,
+ * API version is assumed to be 0.
+ *
+ * In legacy code, the disk control interface is defined by passing
+ * a struct of type retro_disk_control_callback to
+ * RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE.
+ * This may be still be done regardless of the disk control
+ * interface version.
+ *
+ * If version is >= 1 however, the disk control interface may
+ * instead be defined by passing a struct of type
+ * retro_disk_control_ext_callback to
+ * RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE.
+ * This allows the core to provide additional information about
+ * disk images to the frontend and/or enables extra
+ * disk control functionality by the frontend.
+ */
+
+#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE 58
+ /* const struct retro_disk_control_ext_callback * --
+ * Sets an interface which frontend can use to eject and insert
+ * disk images, and also obtain information about individual
+ * disk image files registered by the core.
+ * This is used for games which consist of multiple images and
+ * must be manually swapped out by the user (e.g. PSX, floppy disk
+ * based systems).
+ */
+
+#define RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION 59
+ /* unsigned * --
+ * Unsigned value is the API version number of the message
+ * interface supported by the frontend. If callback returns
+ * false, API version is assumed to be 0.
+ *
+ * In legacy code, messages may be displayed in an
+ * implementation-specific manner by passing a struct
+ * of type retro_message to RETRO_ENVIRONMENT_SET_MESSAGE.
+ * This may be still be done regardless of the message
+ * interface version.
+ *
+ * If version is >= 1 however, messages may instead be
+ * displayed by passing a struct of type retro_message_ext
+ * to RETRO_ENVIRONMENT_SET_MESSAGE_EXT. This allows the
+ * core to specify message logging level, priority and
+ * destination (OSD, logging interface or both).
+ */
+
+#define RETRO_ENVIRONMENT_SET_MESSAGE_EXT 60
+ /* const struct retro_message_ext * --
+ * Sets a message to be displayed in an implementation-specific
+ * manner for a certain amount of 'frames'. Additionally allows
+ * the core to specify message logging level, priority and
+ * destination (OSD, logging interface or both).
+ * Should not be used for trivial messages, which should simply be
+ * logged via RETRO_ENVIRONMENT_GET_LOG_INTERFACE (or as a
+ * fallback, stderr).
+ */
+
+#define RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS 61
+ /* unsigned * --
+ * Unsigned value is the number of active input devices
+ * provided by the frontend. This may change between
+ * frames, but will remain constant for the duration
+ * of each frame.
+ * If callback returns true, a core need not poll any
+ * input device with an index greater than or equal to
+ * the number of active devices.
+ * If callback returns false, the number of active input
+ * devices is unknown. In this case, all input devices
+ * should be considered active.
+ */
+
+#define RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK 62
+ /* const struct retro_audio_buffer_status_callback * --
+ * Lets the core know the occupancy level of the frontend
+ * audio buffer. Can be used by a core to attempt frame
+ * skipping in order to avoid buffer under-runs.
+ * A core may pass NULL to disable buffer status reporting
+ * in the frontend.
+ */
+
+#define RETRO_ENVIRONMENT_SET_MINIMUM_AUDIO_LATENCY 63
+ /* const unsigned * --
+ * Sets minimum frontend audio latency in milliseconds.
+ * Resultant audio latency may be larger than set value,
+ * or smaller if a hardware limit is encountered. A frontend
+ * is expected to honour requests up to 512 ms.
+ *
+ * - If value is less than current frontend
+ * audio latency, callback has no effect
+ * - If value is zero, default frontend audio
+ * latency is set
+ *
+ * May be used by a core to increase audio latency and
+ * therefore decrease the probability of buffer under-runs
+ * (crackling) when performing 'intensive' operations.
+ * A core utilising RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK
+ * to implement audio-buffer-based frame skipping may achieve
+ * optimal results by setting the audio latency to a 'high'
+ * (typically 6x or 8x) integer multiple of the expected
+ * frame time.
+ *
+ * WARNING: This can only be called from within retro_run().
+ * Calling this can require a full reinitialization of audio
+ * drivers in the frontend, so it is important to call it very
+ * sparingly, and usually only with the users explicit consent.
+ * An eventual driver reinitialize will happen so that audio
+ * callbacks happening after this call within the same retro_run()
+ * call will target the newly initialized driver.
+ */
+
+#define RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE 64
+ /* const struct retro_fastforwarding_override * --
+ * Used by a libretro core to override the current
+ * fastforwarding mode of the frontend.
+ * If NULL is passed to this function, the frontend
+ * will return true if fastforwarding override
+ * functionality is supported (no change in
+ * fastforwarding state will occur in this case).
+ */
+
+#define RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE 65
+ /* const struct retro_system_content_info_override * --
+ * Allows an implementation to override 'global' content
+ * info parameters reported by retro_get_system_info().
+ * Overrides also affect subsystem content info parameters
+ * set via RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO.
+ * This function must be called inside retro_set_environment().
+ * If callback returns false, content info overrides
+ * are unsupported by the frontend, and will be ignored.
+ * If callback returns true, extended game info may be
+ * retrieved by calling RETRO_ENVIRONMENT_GET_GAME_INFO_EXT
+ * in retro_load_game() or retro_load_game_special().
+ *
+ * 'data' points to an array of retro_system_content_info_override
+ * structs terminated by a { NULL, false, false } element.
+ * If 'data' is NULL, no changes will be made to the frontend;
+ * a core may therefore pass NULL in order to test whether
+ * the RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE and
+ * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT callbacks are supported
+ * by the frontend.
+ *
+ * For struct member descriptions, see the definition of
+ * struct retro_system_content_info_override.
+ *
+ * Example:
+ *
+ * - struct retro_system_info:
+ * {
+ * "My Core", // library_name
+ * "v1.0", // library_version
+ * "m3u|md|cue|iso|chd|sms|gg|sg", // valid_extensions
+ * true, // need_fullpath
+ * false // block_extract
+ * }
+ *
+ * - Array of struct retro_system_content_info_override:
+ * {
+ * {
+ * "md|sms|gg", // extensions
+ * false, // need_fullpath
+ * true // persistent_data
+ * },
+ * {
+ * "sg", // extensions
+ * false, // need_fullpath
+ * false // persistent_data
+ * },
+ * { NULL, false, false }
+ * }
+ *
+ * Result:
+ * - Files of type m3u, cue, iso, chd will not be
+ * loaded by the frontend. Frontend will pass a
+ * valid path to the core, and core will handle
+ * loading internally
+ * - Files of type md, sms, gg will be loaded by
+ * the frontend. A valid memory buffer will be
+ * passed to the core. This memory buffer will
+ * remain valid until retro_deinit() returns
+ * - Files of type sg will be loaded by the frontend.
+ * A valid memory buffer will be passed to the core.
+ * This memory buffer will remain valid until
+ * retro_load_game() (or retro_load_game_special())
+ * returns
+ *
+ * NOTE: If an extension is listed multiple times in
+ * an array of retro_system_content_info_override
+ * structs, only the first instance will be registered
+ */
+
+#define RETRO_ENVIRONMENT_GET_GAME_INFO_EXT 66
+ /* const struct retro_game_info_ext ** --
+ * Allows an implementation to fetch extended game
+ * information, providing additional content path
+ * and memory buffer status details.
+ * This function may only be called inside
+ * retro_load_game() or retro_load_game_special().
+ * If callback returns false, extended game information
+ * is unsupported by the frontend. In this case, only
+ * regular retro_game_info will be available.
+ * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT is guaranteed
+ * to return true if RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE
+ * returns true.
+ *
+ * 'data' points to an array of retro_game_info_ext structs.
+ *
+ * For struct member descriptions, see the definition of
+ * struct retro_game_info_ext.
+ *
+ * - If function is called inside retro_load_game(),
+ * the retro_game_info_ext array is guaranteed to
+ * have a size of 1 - i.e. the returned pointer may
+ * be used to access directly the members of the
+ * first retro_game_info_ext struct, for example:
+ *
+ * struct retro_game_info_ext *game_info_ext;
+ * if (environ_cb(RETRO_ENVIRONMENT_GET_GAME_INFO_EXT, &game_info_ext))
+ * printf("Content Directory: %s\n", game_info_ext->dir);
+ *
+ * - If the function is called inside retro_load_game_special(),
+ * the retro_game_info_ext array is guaranteed to have a
+ * size equal to the num_info argument passed to
+ * retro_load_game_special()
+ */
+
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 67
+ /* const struct retro_core_options_v2 * --
+ * Allows an implementation to signal the environment
+ * which variables it might want to check for later using
+ * GET_VARIABLE.
+ * This allows the frontend to present these variables to
+ * a user dynamically.
+ * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION
+ * returns an API version of >= 2.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS.
+ * This should be called the first time as early as
+ * possible (ideally in retro_set_environment).
+ * Afterwards it may be called again for the core to communicate
+ * updated options to the frontend, but the number of core
+ * options must not change from the number in the initial call.
+ * If RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION returns an API
+ * version of >= 2, this callback is guaranteed to succeed
+ * (i.e. callback return value does not indicate success)
+ * If callback returns true, frontend has core option category
+ * support.
+ * If callback returns false, frontend does not have core option
+ * category support.
+ *
+ * 'data' points to a retro_core_options_v2 struct, containing
+ * of two pointers:
+ * - retro_core_options_v2::categories is an array of
+ * retro_core_option_v2_category structs terminated by a
+ * { NULL, NULL, NULL } element. If retro_core_options_v2::categories
+ * is NULL, all core options will have no category and will be shown
+ * at the top level of the frontend core option interface. If frontend
+ * does not have core option category support, categories array will
+ * be ignored.
+ * - retro_core_options_v2::definitions is an array of
+ * retro_core_option_v2_definition structs terminated by a
+ * { NULL, NULL, NULL, NULL, NULL, NULL, {{0}}, NULL }
+ * element.
+ *
+ * >> retro_core_option_v2_category notes:
+ *
+ * - retro_core_option_v2_category::key should contain string
+ * that uniquely identifies the core option category. Valid
+ * key characters are [a-z, A-Z, 0-9, _, -]
+ * Namespace collisions with other implementations' category
+ * keys are permitted.
+ * - retro_core_option_v2_category::desc should contain a human
+ * readable description of the category key.
+ * - retro_core_option_v2_category::info should contain any
+ * additional human readable information text that a typical
+ * user may need to understand the nature of the core option
+ * category.
+ *
+ * Example entry:
+ * {
+ * "advanced_settings",
+ * "Advanced",
+ * "Options affecting low-level emulation performance and accuracy."
+ * }
+ *
+ * >> retro_core_option_v2_definition notes:
+ *
+ * - retro_core_option_v2_definition::key should be namespaced to not
+ * collide with other implementations' keys. e.g. A core called
+ * 'foo' should use keys named as 'foo_option'. Valid key characters
+ * are [a-z, A-Z, 0-9, _, -].
+ * - retro_core_option_v2_definition::desc should contain a human readable
+ * description of the key. Will be used when the frontend does not
+ * have core option category support. Examples: "Aspect Ratio" or
+ * "Video > Aspect Ratio".
+ * - retro_core_option_v2_definition::desc_categorized should contain a
+ * human readable description of the key, which will be used when
+ * frontend has core option category support. Example: "Aspect Ratio",
+ * where associated retro_core_option_v2_category::desc is "Video".
+ * If empty or NULL, the string specified by
+ * retro_core_option_v2_definition::desc will be used instead.
+ * retro_core_option_v2_definition::desc_categorized will be ignored
+ * if retro_core_option_v2_definition::category_key is empty or NULL.
+ * - retro_core_option_v2_definition::info should contain any additional
+ * human readable information text that a typical user may need to
+ * understand the functionality of the option.
+ * - retro_core_option_v2_definition::info_categorized should contain
+ * any additional human readable information text that a typical user
+ * may need to understand the functionality of the option, and will be
+ * used when frontend has core option category support. This is provided
+ * to accommodate the case where info text references an option by
+ * name/desc, and the desc/desc_categorized text for that option differ.
+ * If empty or NULL, the string specified by
+ * retro_core_option_v2_definition::info will be used instead.
+ * retro_core_option_v2_definition::info_categorized will be ignored
+ * if retro_core_option_v2_definition::category_key is empty or NULL.
+ * - retro_core_option_v2_definition::category_key should contain a
+ * category identifier (e.g. "video" or "audio") that will be
+ * assigned to the core option if frontend has core option category
+ * support. A categorized option will be shown in a subsection/
+ * submenu of the frontend core option interface. If key is empty
+ * or NULL, or if key does not match one of the
+ * retro_core_option_v2_category::key values in the associated
+ * retro_core_option_v2_category array, option will have no category
+ * and will be shown at the top level of the frontend core option
+ * interface.
+ * - retro_core_option_v2_definition::values is an array of
+ * retro_core_option_value structs terminated by a { NULL, NULL }
+ * element.
+ * --> retro_core_option_v2_definition::values[index].value is an
+ * expected option value.
+ * --> retro_core_option_v2_definition::values[index].label is a
+ * human readable label used when displaying the value on screen.
+ * If NULL, the value itself is used.
+ * - retro_core_option_v2_definition::default_value is the default
+ * core option setting. It must match one of the expected option
+ * values in the retro_core_option_v2_definition::values array. If
+ * it does not, or the default value is NULL, the first entry in the
+ * retro_core_option_v2_definition::values array is treated as the
+ * default.
+ *
+ * The number of possible option values should be very limited,
+ * and must be less than RETRO_NUM_CORE_OPTION_VALUES_MAX.
+ * i.e. it should be feasible to cycle through options
+ * without a keyboard.
+ *
+ * Example entries:
+ *
+ * - Uncategorized:
+ *
+ * {
+ * "foo_option",
+ * "Speed hack coprocessor X",
+ * NULL,
+ * "Provides increased performance at the expense of reduced accuracy.",
+ * NULL,
+ * NULL,
+ * {
+ * { "false", NULL },
+ * { "true", NULL },
+ * { "unstable", "Turbo (Unstable)" },
+ * { NULL, NULL },
+ * },
+ * "false"
+ * }
+ *
+ * - Categorized:
+ *
+ * {
+ * "foo_option",
+ * "Advanced > Speed hack coprocessor X",
+ * "Speed hack coprocessor X",
+ * "Setting 'Advanced > Speed hack coprocessor X' to 'true' or 'Turbo' provides increased performance at the expense of reduced accuracy",
+ * "Setting 'Speed hack coprocessor X' to 'true' or 'Turbo' provides increased performance at the expense of reduced accuracy",
+ * "advanced_settings",
+ * {
+ * { "false", NULL },
+ * { "true", NULL },
+ * { "unstable", "Turbo (Unstable)" },
+ * { NULL, NULL },
+ * },
+ * "false"
+ * }
+ *
+ * Only strings are operated on. The possible values will
+ * generally be displayed and stored as-is by the frontend.
+ */
+
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL 68
+ /* const struct retro_core_options_v2_intl * --
+ * Allows an implementation to signal the environment
+ * which variables it might want to check for later using
+ * GET_VARIABLE.
+ * This allows the frontend to present these variables to
+ * a user dynamically.
+ * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION
+ * returns an API version of >= 2.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL.
+ * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2.
+ * This should be called the first time as early as
+ * possible (ideally in retro_set_environment).
+ * Afterwards it may be called again for the core to communicate
+ * updated options to the frontend, but the number of core
+ * options must not change from the number in the initial call.
+ * If RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION returns an API
+ * version of >= 2, this callback is guaranteed to succeed
+ * (i.e. callback return value does not indicate success)
+ * If callback returns true, frontend has core option category
+ * support.
+ * If callback returns false, frontend does not have core option
+ * category support.
+ *
+ * This is fundamentally the same as RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2,
+ * with the addition of localisation support. The description of the
+ * RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 callback should be consulted
+ * for further details.
+ *
+ * 'data' points to a retro_core_options_v2_intl struct.
+ *
+ * - retro_core_options_v2_intl::us is a pointer to a
+ * retro_core_options_v2 struct defining the US English
+ * core options implementation. It must point to a valid struct.
+ *
+ * - retro_core_options_v2_intl::local is a pointer to a
+ * retro_core_options_v2 struct defining core options for
+ * the current frontend language. It may be NULL (in which case
+ * retro_core_options_v2_intl::us is used by the frontend). Any items
+ * missing from this struct will be read from
+ * retro_core_options_v2_intl::us instead.
+ *
+ * NOTE: Default core option values are always taken from the
+ * retro_core_options_v2_intl::us struct. Any default values in
+ * the retro_core_options_v2_intl::local struct will be ignored.
+ */
+
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK 69
+ /* const struct retro_core_options_update_display_callback * --
+ * Allows a frontend to signal that a core must update
+ * the visibility of any dynamically hidden core options,
+ * and enables the frontend to detect visibility changes.
+ * Used by the frontend to update the menu display status
+ * of core options without requiring a call of retro_run().
+ * Must be called in retro_set_environment().
+ */
+
+#define RETRO_ENVIRONMENT_SET_VARIABLE 70
+ /* const struct retro_variable * --
+ * Allows an implementation to notify the frontend
+ * that a core option value has changed.
+ *
+ * retro_variable::key and retro_variable::value
+ * must match strings that have been set previously
+ * via one of the following:
+ *
+ * - RETRO_ENVIRONMENT_SET_VARIABLES
+ * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS
+ * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL
+ * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL
+ *
+ * After changing a core option value via this
+ * callback, RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE
+ * will return true.
+ *
+ * If data is NULL, no changes will be registered
+ * and the callback will return true; an
+ * implementation may therefore pass NULL in order
+ * to test whether the callback is supported.
+ */
+
+#define RETRO_ENVIRONMENT_GET_THROTTLE_STATE (71 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_throttle_state * --
+ * Allows an implementation to get details on the actual rate
+ * the frontend is attempting to call retro_run().
+ */
+
+#define RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT (72 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* int * --
+ * Tells the core about the context the frontend is asking for savestate.
+ * (see enum retro_savestate_context)
+ */
+
+#define RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT (73 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_hw_render_context_negotiation_interface * --
+ * Before calling SET_HW_RNEDER_CONTEXT_NEGOTIATION_INTERFACE, a core can query
+ * which version of the interface is supported.
+ *
+ * Frontend looks at interface_type and returns the maximum supported
+ * context negotiation interface version.
+ * If the interface_type is not supported or recognized by the frontend, a version of 0
+ * must be returned in interface_version and true is returned by frontend.
+ *
+ * If this environment call returns true with interface_version greater than 0,
+ * a core can always use a negotiation interface version larger than what the frontend returns, but only
+ * earlier versions of the interface will be used by the frontend.
+ * A frontend must not reject a negotiation interface version that is larger than
+ * what the frontend supports. Instead, the frontend will use the older entry points that it recognizes.
+ * If this is incompatible with a particular core's requirements, it can error out early.
+ *
+ * Backwards compatibility note:
+ * This environment call was introduced after Vulkan v1 context negotiation.
+ * If this environment call is not supported by frontend - i.e. the environment call returns false -
+ * only Vulkan v1 context negotiation is supported (if Vulkan HW rendering is supported at all).
+ * If a core uses Vulkan negotiation interface with version > 1, negotiation may fail unexpectedly.
+ * All future updates to the context negotiation interface implies that frontend must support
+ * this environment call to query support.
+ */
+
+
+/* VFS functionality */
+
+/* File paths:
+ * File paths passed as parameters when using this API shall be well formed UNIX-style,
+ * using "/" (unquoted forward slash) as directory separator regardless of the platform's native separator.
+ * Paths shall also include at least one forward slash ("game.bin" is an invalid path, use "./game.bin" instead).
+ * Other than the directory separator, cores shall not make assumptions about path format:
+ * "C:/path/game.bin", "http://example.com/game.bin", "#game/game.bin", "./game.bin" (without quotes) are all valid paths.
+ * Cores may replace the basename or remove path components from the end, and/or add new components;
+ * however, cores shall not append "./", "../" or multiple consecutive forward slashes ("//") to paths they request to front end.
+ * The frontend is encouraged to make such paths work as well as it can, but is allowed to give up if the core alters paths too much.
+ * Frontends are encouraged, but not required, to support native file system paths (modulo replacing the directory separator, if applicable).
+ * Cores are allowed to try using them, but must remain functional if the front rejects such requests.
+ * Cores are encouraged to use the libretro-common filestream functions for file I/O,
+ * as they seamlessly integrate with VFS, deal with directory separator replacement as appropriate
+ * and provide platform-specific fallbacks in cases where front ends do not support VFS. */
+
+/* Opaque file handle
+ * Introduced in VFS API v1 */
+struct retro_vfs_file_handle;
+
+/* Opaque directory handle
+ * Introduced in VFS API v3 */
+struct retro_vfs_dir_handle;
+
+/* File open flags
+ * Introduced in VFS API v1 */
+#define RETRO_VFS_FILE_ACCESS_READ (1 << 0) /* Read only mode */
+#define RETRO_VFS_FILE_ACCESS_WRITE (1 << 1) /* Write only mode, discard contents and overwrites existing file unless RETRO_VFS_FILE_ACCESS_UPDATE is also specified */
+#define RETRO_VFS_FILE_ACCESS_READ_WRITE (RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE) /* Read-write mode, discard contents and overwrites existing file unless RETRO_VFS_FILE_ACCESS_UPDATE is also specified*/
+#define RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING (1 << 2) /* Prevents discarding content of existing files opened for writing */
+
+/* These are only hints. The frontend may choose to ignore them. Other than RAM/CPU/etc use,
+ and how they react to unlikely external interference (for example someone else writing to that file,
+ or the file's server going down), behavior will not change. */
+#define RETRO_VFS_FILE_ACCESS_HINT_NONE (0)
+/* Indicate that the file will be accessed many times. The frontend should aggressively cache everything. */
+#define RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS (1 << 0)
+
+/* Seek positions */
+#define RETRO_VFS_SEEK_POSITION_START 0
+#define RETRO_VFS_SEEK_POSITION_CURRENT 1
+#define RETRO_VFS_SEEK_POSITION_END 2
+
+/* stat() result flags
+ * Introduced in VFS API v3 */
+#define RETRO_VFS_STAT_IS_VALID (1 << 0)
+#define RETRO_VFS_STAT_IS_DIRECTORY (1 << 1)
+#define RETRO_VFS_STAT_IS_CHARACTER_SPECIAL (1 << 2)
+
+/* Get path from opaque handle. Returns the exact same path passed to file_open when getting the handle
+ * Introduced in VFS API v1 */
+typedef const char *(RETRO_CALLCONV *retro_vfs_get_path_t)(struct retro_vfs_file_handle *stream);
+
+/* Open a file for reading or writing. If path points to a directory, this will
+ * fail. Returns the opaque file handle, or NULL for error.
+ * Introduced in VFS API v1 */
+typedef struct retro_vfs_file_handle *(RETRO_CALLCONV *retro_vfs_open_t)(const char *path, unsigned mode, unsigned hints);
+
+/* Close the file and release its resources. Must be called if open_file returns non-NULL. Returns 0 on success, -1 on failure.
+ * Whether the call succeeds ot not, the handle passed as parameter becomes invalid and should no longer be used.
+ * Introduced in VFS API v1 */
+typedef int (RETRO_CALLCONV *retro_vfs_close_t)(struct retro_vfs_file_handle *stream);
+
+/* Return the size of the file in bytes, or -1 for error.
+ * Introduced in VFS API v1 */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_size_t)(struct retro_vfs_file_handle *stream);
+
+/* Truncate file to specified size. Returns 0 on success or -1 on error
+ * Introduced in VFS API v2 */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_truncate_t)(struct retro_vfs_file_handle *stream, int64_t length);
+
+/* Get the current read / write position for the file. Returns -1 for error.
+ * Introduced in VFS API v1 */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_tell_t)(struct retro_vfs_file_handle *stream);
+
+/* Set the current read/write position for the file. Returns the new position, -1 for error.
+ * Introduced in VFS API v1 */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_seek_t)(struct retro_vfs_file_handle *stream, int64_t offset, int seek_position);
+
+/* Read data from a file. Returns the number of bytes read, or -1 for error.
+ * Introduced in VFS API v1 */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_read_t)(struct retro_vfs_file_handle *stream, void *s, uint64_t len);
+
+/* Write data to a file. Returns the number of bytes written, or -1 for error.
+ * Introduced in VFS API v1 */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_write_t)(struct retro_vfs_file_handle *stream, const void *s, uint64_t len);
+
+/* Flush pending writes to file, if using buffered IO. Returns 0 on sucess, or -1 on failure.
+ * Introduced in VFS API v1 */
+typedef int (RETRO_CALLCONV *retro_vfs_flush_t)(struct retro_vfs_file_handle *stream);
+
+/* Delete the specified file. Returns 0 on success, -1 on failure
+ * Introduced in VFS API v1 */
+typedef int (RETRO_CALLCONV *retro_vfs_remove_t)(const char *path);
+
+/* Rename the specified file. Returns 0 on success, -1 on failure
+ * Introduced in VFS API v1 */
+typedef int (RETRO_CALLCONV *retro_vfs_rename_t)(const char *old_path, const char *new_path);
+
+/* Stat the specified file. Retruns a bitmask of RETRO_VFS_STAT_* flags, none are set if path was not valid.
+ * Additionally stores file size in given variable, unless NULL is given.
+ * Introduced in VFS API v3 */
+typedef int (RETRO_CALLCONV *retro_vfs_stat_t)(const char *path, int32_t *size);
+
+/* Create the specified directory. Returns 0 on success, -1 on unknown failure, -2 if already exists.
+ * Introduced in VFS API v3 */
+typedef int (RETRO_CALLCONV *retro_vfs_mkdir_t)(const char *dir);
+
+/* Open the specified directory for listing. Returns the opaque dir handle, or NULL for error.
+ * Support for the include_hidden argument may vary depending on the platform.
+ * Introduced in VFS API v3 */
+typedef struct retro_vfs_dir_handle *(RETRO_CALLCONV *retro_vfs_opendir_t)(const char *dir, bool include_hidden);
+
+/* Read the directory entry at the current position, and move the read pointer to the next position.
+ * Returns true on success, false if already on the last entry.
+ * Introduced in VFS API v3 */
+typedef bool (RETRO_CALLCONV *retro_vfs_readdir_t)(struct retro_vfs_dir_handle *dirstream);
+
+/* Get the name of the last entry read. Returns a string on success, or NULL for error.
+ * The returned string pointer is valid until the next call to readdir or closedir.
+ * Introduced in VFS API v3 */
+typedef const char *(RETRO_CALLCONV *retro_vfs_dirent_get_name_t)(struct retro_vfs_dir_handle *dirstream);
+
+/* Check if the last entry read was a directory. Returns true if it was, false otherwise (or on error).
+ * Introduced in VFS API v3 */
+typedef bool (RETRO_CALLCONV *retro_vfs_dirent_is_dir_t)(struct retro_vfs_dir_handle *dirstream);
+
+/* Close the directory and release its resources. Must be called if opendir returns non-NULL. Returns 0 on success, -1 on failure.
+ * Whether the call succeeds ot not, the handle passed as parameter becomes invalid and should no longer be used.
+ * Introduced in VFS API v3 */
+typedef int (RETRO_CALLCONV *retro_vfs_closedir_t)(struct retro_vfs_dir_handle *dirstream);
+
+struct retro_vfs_interface
+{
+ /* VFS API v1 */
+ retro_vfs_get_path_t get_path;
+ retro_vfs_open_t open;
+ retro_vfs_close_t close;
+ retro_vfs_size_t size;
+ retro_vfs_tell_t tell;
+ retro_vfs_seek_t seek;
+ retro_vfs_read_t read;
+ retro_vfs_write_t write;
+ retro_vfs_flush_t flush;
+ retro_vfs_remove_t remove;
+ retro_vfs_rename_t rename;
+ /* VFS API v2 */
+ retro_vfs_truncate_t truncate;
+ /* VFS API v3 */
+ retro_vfs_stat_t stat;
+ retro_vfs_mkdir_t mkdir;
+ retro_vfs_opendir_t opendir;
+ retro_vfs_readdir_t readdir;
+ retro_vfs_dirent_get_name_t dirent_get_name;
+ retro_vfs_dirent_is_dir_t dirent_is_dir;
+ retro_vfs_closedir_t closedir;
+};
+
+struct retro_vfs_interface_info
+{
+ /* Set by core: should this be higher than the version the front end supports,
+ * front end will return false in the RETRO_ENVIRONMENT_GET_VFS_INTERFACE call
+ * Introduced in VFS API v1 */
+ uint32_t required_interface_version;
+
+ /* Frontend writes interface pointer here. The frontend also sets the actual
+ * version, must be at least required_interface_version.
+ * Introduced in VFS API v1 */
+ struct retro_vfs_interface *iface;
+};
+
+enum retro_hw_render_interface_type
+{
+ RETRO_HW_RENDER_INTERFACE_VULKAN = 0,
+ RETRO_HW_RENDER_INTERFACE_D3D9 = 1,
+ RETRO_HW_RENDER_INTERFACE_D3D10 = 2,
+ RETRO_HW_RENDER_INTERFACE_D3D11 = 3,
+ RETRO_HW_RENDER_INTERFACE_D3D12 = 4,
+ RETRO_HW_RENDER_INTERFACE_GSKIT_PS2 = 5,
+ RETRO_HW_RENDER_INTERFACE_DUMMY = INT_MAX
+};
+
+/* Base struct. All retro_hw_render_interface_* types
+ * contain at least these fields. */
+struct retro_hw_render_interface
+{
+ enum retro_hw_render_interface_type interface_type;
+ unsigned interface_version;
+};
+
+typedef void (RETRO_CALLCONV *retro_set_led_state_t)(int led, int state);
+struct retro_led_interface
+{
+ retro_set_led_state_t set_led_state;
+};
+
+/* Retrieves the current state of the MIDI input.
+ * Returns true if it's enabled, false otherwise. */
+typedef bool (RETRO_CALLCONV *retro_midi_input_enabled_t)(void);
+
+/* Retrieves the current state of the MIDI output.
+ * Returns true if it's enabled, false otherwise */
+typedef bool (RETRO_CALLCONV *retro_midi_output_enabled_t)(void);
+
+/* Reads next byte from the input stream.
+ * Returns true if byte is read, false otherwise. */
+typedef bool (RETRO_CALLCONV *retro_midi_read_t)(uint8_t *byte);
+
+/* Writes byte to the output stream.
+ * 'delta_time' is in microseconds and represent time elapsed since previous write.
+ * Returns true if byte is written, false otherwise. */
+typedef bool (RETRO_CALLCONV *retro_midi_write_t)(uint8_t byte, uint32_t delta_time);
+
+/* Flushes previously written data.
+ * Returns true if successful, false otherwise. */
+typedef bool (RETRO_CALLCONV *retro_midi_flush_t)(void);
+
+struct retro_midi_interface
+{
+ retro_midi_input_enabled_t input_enabled;
+ retro_midi_output_enabled_t output_enabled;
+ retro_midi_read_t read;
+ retro_midi_write_t write;
+ retro_midi_flush_t flush;
+};
+
+enum retro_hw_render_context_negotiation_interface_type
+{
+ RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN = 0,
+ RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_DUMMY = INT_MAX
+};
+
+/* Base struct. All retro_hw_render_context_negotiation_interface_* types
+ * contain at least these fields. */
+struct retro_hw_render_context_negotiation_interface
+{
+ enum retro_hw_render_context_negotiation_interface_type interface_type;
+ unsigned interface_version;
+};
+
+/* Serialized state is incomplete in some way. Set if serialization is
+ * usable in typical end-user cases but should not be relied upon to
+ * implement frame-sensitive frontend features such as netplay or
+ * rerecording. */
+#define RETRO_SERIALIZATION_QUIRK_INCOMPLETE (1 << 0)
+/* The core must spend some time initializing before serialization is
+ * supported. retro_serialize() will initially fail; retro_unserialize()
+ * and retro_serialize_size() may or may not work correctly either. */
+#define RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE (1 << 1)
+/* Serialization size may change within a session. */
+#define RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE (1 << 2)
+/* Set by the frontend to acknowledge that it supports variable-sized
+ * states. */
+#define RETRO_SERIALIZATION_QUIRK_FRONT_VARIABLE_SIZE (1 << 3)
+/* Serialized state can only be loaded during the same session. */
+#define RETRO_SERIALIZATION_QUIRK_SINGLE_SESSION (1 << 4)
+/* Serialized state cannot be loaded on an architecture with a different
+ * endianness from the one it was saved on. */
+#define RETRO_SERIALIZATION_QUIRK_ENDIAN_DEPENDENT (1 << 5)
+/* Serialized state cannot be loaded on a different platform from the one it
+ * was saved on for reasons other than endianness, such as word size
+ * dependence */
+#define RETRO_SERIALIZATION_QUIRK_PLATFORM_DEPENDENT (1 << 6)
+
+#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */
+#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */
+#define RETRO_MEMDESC_SYSTEM_RAM (1 << 2) /* The memory area is system RAM. This is main RAM of the gaming system. */
+#define RETRO_MEMDESC_SAVE_RAM (1 << 3) /* The memory area is save RAM. This RAM is usually found on a game cartridge, backed up by a battery. */
+#define RETRO_MEMDESC_VIDEO_RAM (1 << 4) /* The memory area is video RAM (VRAM) */
+#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */
+#define RETRO_MEMDESC_ALIGN_4 (2 << 16)
+#define RETRO_MEMDESC_ALIGN_8 (3 << 16)
+#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */
+#define RETRO_MEMDESC_MINSIZE_4 (2 << 24)
+#define RETRO_MEMDESC_MINSIZE_8 (3 << 24)
+struct retro_memory_descriptor
+{
+ uint64_t flags;
+
+ /* Pointer to the start of the relevant ROM or RAM chip.
+ * It's strongly recommended to use 'offset' if possible, rather than
+ * doing math on the pointer.
+ *
+ * If the same byte is mapped my multiple descriptors, their descriptors
+ * must have the same pointer.
+ * If 'start' does not point to the first byte in the pointer, put the
+ * difference in 'offset' instead.
+ *
+ * May be NULL if there's nothing usable here (e.g. hardware registers and
+ * open bus). No flags should be set if the pointer is NULL.
+ * It's recommended to minimize the number of descriptors if possible,
+ * but not mandatory. */
+ void *ptr;
+ size_t offset;
+
+ /* This is the location in the emulated address space
+ * where the mapping starts. */
+ size_t start;
+
+ /* Which bits must be same as in 'start' for this mapping to apply.
+ * The first memory descriptor to claim a certain byte is the one
+ * that applies.
+ * A bit which is set in 'start' must also be set in this.
+ * Can be zero, in which case each byte is assumed mapped exactly once.
+ * In this case, 'len' must be a power of two. */
+ size_t select;
+
+ /* If this is nonzero, the set bits are assumed not connected to the
+ * memory chip's address pins. */
+ size_t disconnect;
+
+ /* This one tells the size of the current memory area.
+ * If, after start+disconnect are applied, the address is higher than
+ * this, the highest bit of the address is cleared.
+ *
+ * If the address is still too high, the next highest bit is cleared.
+ * Can be zero, in which case it's assumed to be infinite (as limited
+ * by 'select' and 'disconnect'). */
+ size_t len;
+
+ /* To go from emulated address to physical address, the following
+ * order applies:
+ * Subtract 'start', pick off 'disconnect', apply 'len', add 'offset'. */
+
+ /* The address space name must consist of only a-zA-Z0-9_-,
+ * should be as short as feasible (maximum length is 8 plus the NUL),
+ * and may not be any other address space plus one or more 0-9A-F
+ * at the end.
+ * However, multiple memory descriptors for the same address space is
+ * allowed, and the address space name can be empty. NULL is treated
+ * as empty.
+ *
+ * Address space names are case sensitive, but avoid lowercase if possible.
+ * The same pointer may exist in multiple address spaces.
+ *
+ * Examples:
+ * blank+blank - valid (multiple things may be mapped in the same namespace)
+ * 'Sp'+'Sp' - valid (multiple things may be mapped in the same namespace)
+ * 'A'+'B' - valid (neither is a prefix of each other)
+ * 'S'+blank - valid ('S' is not in 0-9A-F)
+ * 'a'+blank - valid ('a' is not in 0-9A-F)
+ * 'a'+'A' - valid (neither is a prefix of each other)
+ * 'AR'+blank - valid ('R' is not in 0-9A-F)
+ * 'ARB'+blank - valid (the B can't be part of the address either, because
+ * there is no namespace 'AR')
+ * blank+'B' - not valid, because it's ambigous which address space B1234
+ * would refer to.
+ * The length can't be used for that purpose; the frontend may want
+ * to append arbitrary data to an address, without a separator. */
+ const char *addrspace;
+
+ /* TODO: When finalizing this one, add a description field, which should be
+ * "WRAM" or something roughly equally long. */
+
+ /* TODO: When finalizing this one, replace 'select' with 'limit', which tells
+ * which bits can vary and still refer to the same address (limit = ~select).
+ * TODO: limit? range? vary? something else? */
+
+ /* TODO: When finalizing this one, if 'len' is above what 'select' (or
+ * 'limit') allows, it's bankswitched. Bankswitched data must have both 'len'
+ * and 'select' != 0, and the mappings don't tell how the system switches the
+ * banks. */
+
+ /* TODO: When finalizing this one, fix the 'len' bit removal order.
+ * For len=0x1800, pointer 0x1C00 should go to 0x1400, not 0x0C00.
+ * Algorithm: Take bits highest to lowest, but if it goes above len, clear
+ * the most recent addition and continue on the next bit.
+ * TODO: Can the above be optimized? Is "remove the lowest bit set in both
+ * pointer and 'len'" equivalent? */
+
+ /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing
+ * the emulated memory in 32-bit chunks, native endian. But that's nothing
+ * compared to Darek Mihocka
+ * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE
+ * RAM backwards! I'll want to represent both of those, via some flags.
+ *
+ * I suspect MAME either didn't think of that idea, or don't want the #ifdef.
+ * Not sure which, nor do I really care. */
+
+ /* TODO: Some of those flags are unused and/or don't really make sense. Clean
+ * them up. */
+};
+
+/* The frontend may use the largest value of 'start'+'select' in a
+ * certain namespace to infer the size of the address space.
+ *
+ * If the address space is larger than that, a mapping with .ptr=NULL
+ * should be at the end of the array, with .select set to all ones for
+ * as long as the address space is big.
+ *
+ * Sample descriptors (minus .ptr, and RETRO_MEMFLAG_ on the flags):
+ * SNES WRAM:
+ * .start=0x7E0000, .len=0x20000
+ * (Note that this must be mapped before the ROM in most cases; some of the
+ * ROM mappers
+ * try to claim $7E0000, or at least $7E8000.)
+ * SNES SPC700 RAM:
+ * .addrspace="S", .len=0x10000
+ * SNES WRAM mirrors:
+ * .flags=MIRROR, .start=0x000000, .select=0xC0E000, .len=0x2000
+ * .flags=MIRROR, .start=0x800000, .select=0xC0E000, .len=0x2000
+ * SNES WRAM mirrors, alternate equivalent descriptor:
+ * .flags=MIRROR, .select=0x40E000, .disconnect=~0x1FFF
+ * (Various similar constructions can be created by combining parts of
+ * the above two.)
+ * SNES LoROM (512KB, mirrored a couple of times):
+ * .flags=CONST, .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024
+ * .flags=CONST, .start=0x400000, .select=0x400000, .disconnect=0x8000, .len=512*1024
+ * SNES HiROM (4MB):
+ * .flags=CONST, .start=0x400000, .select=0x400000, .len=4*1024*1024
+ * .flags=CONST, .offset=0x8000, .start=0x008000, .select=0x408000, .len=4*1024*1024
+ * SNES ExHiROM (8MB):
+ * .flags=CONST, .offset=0, .start=0xC00000, .select=0xC00000, .len=4*1024*1024
+ * .flags=CONST, .offset=4*1024*1024, .start=0x400000, .select=0xC00000, .len=4*1024*1024
+ * .flags=CONST, .offset=0x8000, .start=0x808000, .select=0xC08000, .len=4*1024*1024
+ * .flags=CONST, .offset=4*1024*1024+0x8000, .start=0x008000, .select=0xC08000, .len=4*1024*1024
+ * Clarify the size of the address space:
+ * .ptr=NULL, .select=0xFFFFFF
+ * .len can be implied by .select in many of them, but was included for clarity.
+ */
+
+struct retro_memory_map
+{
+ const struct retro_memory_descriptor *descriptors;
+ unsigned num_descriptors;
+};
+
+struct retro_controller_description
+{
+ /* Human-readable description of the controller. Even if using a generic
+ * input device type, this can be set to the particular device type the
+ * core uses. */
+ const char *desc;
+
+ /* Device type passed to retro_set_controller_port_device(). If the device
+ * type is a sub-class of a generic input device type, use the
+ * RETRO_DEVICE_SUBCLASS macro to create an ID.
+ *
+ * E.g. RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_JOYPAD, 1). */
+ unsigned id;
+};
+
+struct retro_controller_info
+{
+ const struct retro_controller_description *types;
+ unsigned num_types;
+};
+
+struct retro_subsystem_memory_info
+{
+ /* The extension associated with a memory type, e.g. "psram". */
+ const char *extension;
+
+ /* The memory type for retro_get_memory(). This should be at
+ * least 0x100 to avoid conflict with standardized
+ * libretro memory types. */
+ unsigned type;
+};
+
+struct retro_subsystem_rom_info
+{
+ /* Describes what the content is (SGB BIOS, GB ROM, etc). */
+ const char *desc;
+
+ /* Same definition as retro_get_system_info(). */
+ const char *valid_extensions;
+
+ /* Same definition as retro_get_system_info(). */
+ bool need_fullpath;
+
+ /* Same definition as retro_get_system_info(). */
+ bool block_extract;
+
+ /* This is set if the content is required to load a game.
+ * If this is set to false, a zeroed-out retro_game_info can be passed. */
+ bool required;
+
+ /* Content can have multiple associated persistent
+ * memory types (retro_get_memory()). */
+ const struct retro_subsystem_memory_info *memory;
+ unsigned num_memory;
+};
+
+struct retro_subsystem_info
+{
+ /* Human-readable string of the subsystem type, e.g. "Super GameBoy" */
+ const char *desc;
+
+ /* A computer friendly short string identifier for the subsystem type.
+ * This name must be [a-z].
+ * E.g. if desc is "Super GameBoy", this can be "sgb".
+ * This identifier can be used for command-line interfaces, etc.
+ */
+ const char *ident;
+
+ /* Infos for each content file. The first entry is assumed to be the
+ * "most significant" content for frontend purposes.
+ * E.g. with Super GameBoy, the first content should be the GameBoy ROM,
+ * as it is the most "significant" content to a user.
+ * If a frontend creates new file paths based on the content used
+ * (e.g. savestates), it should use the path for the first ROM to do so. */
+ const struct retro_subsystem_rom_info *roms;
+
+ /* Number of content files associated with a subsystem. */
+ unsigned num_roms;
+
+ /* The type passed to retro_load_game_special(). */
+ unsigned id;
+};
+
+typedef void (RETRO_CALLCONV *retro_proc_address_t)(void);
+
+/* libretro API extension functions:
+ * (None here so far).
+ *
+ * Get a symbol from a libretro core.
+ * Cores should only return symbols which are actual
+ * extensions to the libretro API.
+ *
+ * Frontends should not use this to obtain symbols to standard
+ * libretro entry points (static linking or dlsym).
+ *
+ * The symbol name must be equal to the function name,
+ * e.g. if void retro_foo(void); exists, the symbol must be called "retro_foo".
+ * The returned function pointer must be cast to the corresponding type.
+ */
+typedef retro_proc_address_t (RETRO_CALLCONV *retro_get_proc_address_t)(const char *sym);
+
+struct retro_get_proc_address_interface
+{
+ retro_get_proc_address_t get_proc_address;
+};
+
+enum retro_log_level
+{
+ RETRO_LOG_DEBUG = 0,
+ RETRO_LOG_INFO,
+ RETRO_LOG_WARN,
+ RETRO_LOG_ERROR,
+
+ RETRO_LOG_DUMMY = INT_MAX
+};
+
+/* Logging function. Takes log level argument as well. */
+typedef void (RETRO_CALLCONV *retro_log_printf_t)(enum retro_log_level level,
+ const char *fmt, ...);
+
+struct retro_log_callback
+{
+ retro_log_printf_t log;
+};
+
+/* Performance related functions */
+
+/* ID values for SIMD CPU features */
+#define RETRO_SIMD_SSE (1 << 0)
+#define RETRO_SIMD_SSE2 (1 << 1)
+#define RETRO_SIMD_VMX (1 << 2)
+#define RETRO_SIMD_VMX128 (1 << 3)
+#define RETRO_SIMD_AVX (1 << 4)
+#define RETRO_SIMD_NEON (1 << 5)
+#define RETRO_SIMD_SSE3 (1 << 6)
+#define RETRO_SIMD_SSSE3 (1 << 7)
+#define RETRO_SIMD_MMX (1 << 8)
+#define RETRO_SIMD_MMXEXT (1 << 9)
+#define RETRO_SIMD_SSE4 (1 << 10)
+#define RETRO_SIMD_SSE42 (1 << 11)
+#define RETRO_SIMD_AVX2 (1 << 12)
+#define RETRO_SIMD_VFPU (1 << 13)
+#define RETRO_SIMD_PS (1 << 14)
+#define RETRO_SIMD_AES (1 << 15)
+#define RETRO_SIMD_VFPV3 (1 << 16)
+#define RETRO_SIMD_VFPV4 (1 << 17)
+#define RETRO_SIMD_POPCNT (1 << 18)
+#define RETRO_SIMD_MOVBE (1 << 19)
+#define RETRO_SIMD_CMOV (1 << 20)
+#define RETRO_SIMD_ASIMD (1 << 21)
+
+typedef uint64_t retro_perf_tick_t;
+typedef int64_t retro_time_t;
+
+struct retro_perf_counter
+{
+ const char *ident;
+ retro_perf_tick_t start;
+ retro_perf_tick_t total;
+ retro_perf_tick_t call_cnt;
+
+ bool registered;
+};
+
+/* Returns current time in microseconds.
+ * Tries to use the most accurate timer available.
+ */
+typedef retro_time_t (RETRO_CALLCONV *retro_perf_get_time_usec_t)(void);
+
+/* A simple counter. Usually nanoseconds, but can also be CPU cycles.
+ * Can be used directly if desired (when creating a more sophisticated
+ * performance counter system).
+ * */
+typedef retro_perf_tick_t (RETRO_CALLCONV *retro_perf_get_counter_t)(void);
+
+/* Returns a bit-mask of detected CPU features (RETRO_SIMD_*). */
+typedef uint64_t (RETRO_CALLCONV *retro_get_cpu_features_t)(void);
+
+/* Asks frontend to log and/or display the state of performance counters.
+ * Performance counters can always be poked into manually as well.
+ */
+typedef void (RETRO_CALLCONV *retro_perf_log_t)(void);
+
+/* Register a performance counter.
+ * ident field must be set with a discrete value and other values in
+ * retro_perf_counter must be 0.
+ * Registering can be called multiple times. To avoid calling to
+ * frontend redundantly, you can check registered field first. */
+typedef void (RETRO_CALLCONV *retro_perf_register_t)(struct retro_perf_counter *counter);
+
+/* Starts a registered counter. */
+typedef void (RETRO_CALLCONV *retro_perf_start_t)(struct retro_perf_counter *counter);
+
+/* Stops a registered counter. */
+typedef void (RETRO_CALLCONV *retro_perf_stop_t)(struct retro_perf_counter *counter);
+
+/* For convenience it can be useful to wrap register, start and stop in macros.
+ * E.g.:
+ * #ifdef LOG_PERFORMANCE
+ * #define RETRO_PERFORMANCE_INIT(perf_cb, name) static struct retro_perf_counter name = {#name}; if (!name.registered) perf_cb.perf_register(&(name))
+ * #define RETRO_PERFORMANCE_START(perf_cb, name) perf_cb.perf_start(&(name))
+ * #define RETRO_PERFORMANCE_STOP(perf_cb, name) perf_cb.perf_stop(&(name))
+ * #else
+ * ... Blank macros ...
+ * #endif
+ *
+ * These can then be used mid-functions around code snippets.
+ *
+ * extern struct retro_perf_callback perf_cb; * Somewhere in the core.
+ *
+ * void do_some_heavy_work(void)
+ * {
+ * RETRO_PERFORMANCE_INIT(cb, work_1;
+ * RETRO_PERFORMANCE_START(cb, work_1);
+ * heavy_work_1();
+ * RETRO_PERFORMANCE_STOP(cb, work_1);
+ *
+ * RETRO_PERFORMANCE_INIT(cb, work_2);
+ * RETRO_PERFORMANCE_START(cb, work_2);
+ * heavy_work_2();
+ * RETRO_PERFORMANCE_STOP(cb, work_2);
+ * }
+ *
+ * void retro_deinit(void)
+ * {
+ * perf_cb.perf_log(); * Log all perf counters here for example.
+ * }
+ */
+
+struct retro_perf_callback
+{
+ retro_perf_get_time_usec_t get_time_usec;
+ retro_get_cpu_features_t get_cpu_features;
+
+ retro_perf_get_counter_t get_perf_counter;
+ retro_perf_register_t perf_register;
+ retro_perf_start_t perf_start;
+ retro_perf_stop_t perf_stop;
+ retro_perf_log_t perf_log;
+};
+
+/* FIXME: Document the sensor API and work out behavior.
+ * It will be marked as experimental until then.
+ */
+enum retro_sensor_action
+{
+ RETRO_SENSOR_ACCELEROMETER_ENABLE = 0,
+ RETRO_SENSOR_ACCELEROMETER_DISABLE,
+ RETRO_SENSOR_GYROSCOPE_ENABLE,
+ RETRO_SENSOR_GYROSCOPE_DISABLE,
+ RETRO_SENSOR_ILLUMINANCE_ENABLE,
+ RETRO_SENSOR_ILLUMINANCE_DISABLE,
+
+ RETRO_SENSOR_DUMMY = INT_MAX
+};
+
+/* Id values for SENSOR types. */
+#define RETRO_SENSOR_ACCELEROMETER_X 0
+#define RETRO_SENSOR_ACCELEROMETER_Y 1
+#define RETRO_SENSOR_ACCELEROMETER_Z 2
+#define RETRO_SENSOR_GYROSCOPE_X 3
+#define RETRO_SENSOR_GYROSCOPE_Y 4
+#define RETRO_SENSOR_GYROSCOPE_Z 5
+#define RETRO_SENSOR_ILLUMINANCE 6
+
+typedef bool (RETRO_CALLCONV *retro_set_sensor_state_t)(unsigned port,
+ enum retro_sensor_action action, unsigned rate);
+
+typedef float (RETRO_CALLCONV *retro_sensor_get_input_t)(unsigned port, unsigned id);
+
+struct retro_sensor_interface
+{
+ retro_set_sensor_state_t set_sensor_state;
+ retro_sensor_get_input_t get_sensor_input;
+};
+
+enum retro_camera_buffer
+{
+ RETRO_CAMERA_BUFFER_OPENGL_TEXTURE = 0,
+ RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER,
+
+ RETRO_CAMERA_BUFFER_DUMMY = INT_MAX
+};
+
+/* Starts the camera driver. Can only be called in retro_run(). */
+typedef bool (RETRO_CALLCONV *retro_camera_start_t)(void);
+
+/* Stops the camera driver. Can only be called in retro_run(). */
+typedef void (RETRO_CALLCONV *retro_camera_stop_t)(void);
+
+/* Callback which signals when the camera driver is initialized
+ * and/or deinitialized.
+ * retro_camera_start_t can be called in initialized callback.
+ */
+typedef void (RETRO_CALLCONV *retro_camera_lifetime_status_t)(void);
+
+/* A callback for raw framebuffer data. buffer points to an XRGB8888 buffer.
+ * Width, height and pitch are similar to retro_video_refresh_t.
+ * First pixel is top-left origin.
+ */
+typedef void (RETRO_CALLCONV *retro_camera_frame_raw_framebuffer_t)(const uint32_t *buffer,
+ unsigned width, unsigned height, size_t pitch);
+
+/* A callback for when OpenGL textures are used.
+ *
+ * texture_id is a texture owned by camera driver.
+ * Its state or content should be considered immutable, except for things like
+ * texture filtering and clamping.
+ *
+ * texture_target is the texture target for the GL texture.
+ * These can include e.g. GL_TEXTURE_2D, GL_TEXTURE_RECTANGLE, and possibly
+ * more depending on extensions.
+ *
+ * affine points to a packed 3x3 column-major matrix used to apply an affine
+ * transform to texture coordinates. (affine_matrix * vec3(coord_x, coord_y, 1.0))
+ * After transform, normalized texture coord (0, 0) should be bottom-left
+ * and (1, 1) should be top-right (or (width, height) for RECTANGLE).
+ *
+ * GL-specific typedefs are avoided here to avoid relying on gl.h in
+ * the API definition.
+ */
+typedef void (RETRO_CALLCONV *retro_camera_frame_opengl_texture_t)(unsigned texture_id,
+ unsigned texture_target, const float *affine);
+
+struct retro_camera_callback
+{
+ /* Set by libretro core.
+ * Example bitmask: caps = (1 << RETRO_CAMERA_BUFFER_OPENGL_TEXTURE) | (1 << RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER).
+ */
+ uint64_t caps;
+
+ /* Desired resolution for camera. Is only used as a hint. */
+ unsigned width;
+ unsigned height;
+
+ /* Set by frontend. */
+ retro_camera_start_t start;
+ retro_camera_stop_t stop;
+
+ /* Set by libretro core if raw framebuffer callbacks will be used. */
+ retro_camera_frame_raw_framebuffer_t frame_raw_framebuffer;
+
+ /* Set by libretro core if OpenGL texture callbacks will be used. */
+ retro_camera_frame_opengl_texture_t frame_opengl_texture;
+
+ /* Set by libretro core. Called after camera driver is initialized and
+ * ready to be started.
+ * Can be NULL, in which this callback is not called.
+ */
+ retro_camera_lifetime_status_t initialized;
+
+ /* Set by libretro core. Called right before camera driver is
+ * deinitialized.
+ * Can be NULL, in which this callback is not called.
+ */
+ retro_camera_lifetime_status_t deinitialized;
+};
+
+/* Sets the interval of time and/or distance at which to update/poll
+ * location-based data.
+ *
+ * To ensure compatibility with all location-based implementations,
+ * values for both interval_ms and interval_distance should be provided.
+ *
+ * interval_ms is the interval expressed in milliseconds.
+ * interval_distance is the distance interval expressed in meters.
+ */
+typedef void (RETRO_CALLCONV *retro_location_set_interval_t)(unsigned interval_ms,
+ unsigned interval_distance);
+
+/* Start location services. The device will start listening for changes to the
+ * current location at regular intervals (which are defined with
+ * retro_location_set_interval_t). */
+typedef bool (RETRO_CALLCONV *retro_location_start_t)(void);
+
+/* Stop location services. The device will stop listening for changes
+ * to the current location. */
+typedef void (RETRO_CALLCONV *retro_location_stop_t)(void);
+
+/* Get the position of the current location. Will set parameters to
+ * 0 if no new location update has happened since the last time. */
+typedef bool (RETRO_CALLCONV *retro_location_get_position_t)(double *lat, double *lon,
+ double *horiz_accuracy, double *vert_accuracy);
+
+/* Callback which signals when the location driver is initialized
+ * and/or deinitialized.
+ * retro_location_start_t can be called in initialized callback.
+ */
+typedef void (RETRO_CALLCONV *retro_location_lifetime_status_t)(void);
+
+struct retro_location_callback
+{
+ retro_location_start_t start;
+ retro_location_stop_t stop;
+ retro_location_get_position_t get_position;
+ retro_location_set_interval_t set_interval;
+
+ retro_location_lifetime_status_t initialized;
+ retro_location_lifetime_status_t deinitialized;
+};
+
+enum retro_rumble_effect
+{
+ RETRO_RUMBLE_STRONG = 0,
+ RETRO_RUMBLE_WEAK = 1,
+
+ RETRO_RUMBLE_DUMMY = INT_MAX
+};
+
+/* Sets rumble state for joypad plugged in port 'port'.
+ * Rumble effects are controlled independently,
+ * and setting e.g. strong rumble does not override weak rumble.
+ * Strength has a range of [0, 0xffff].
+ *
+ * Returns true if rumble state request was honored.
+ * Calling this before first retro_run() is likely to return false. */
+typedef bool (RETRO_CALLCONV *retro_set_rumble_state_t)(unsigned port,
+ enum retro_rumble_effect effect, uint16_t strength);
+
+struct retro_rumble_interface
+{
+ retro_set_rumble_state_t set_rumble_state;
+};
+
+/* Notifies libretro that audio data should be written. */
+typedef void (RETRO_CALLCONV *retro_audio_callback_t)(void);
+
+/* True: Audio driver in frontend is active, and callback is
+ * expected to be called regularily.
+ * False: Audio driver in frontend is paused or inactive.
+ * Audio callback will not be called until set_state has been
+ * called with true.
+ * Initial state is false (inactive).
+ */
+typedef void (RETRO_CALLCONV *retro_audio_set_state_callback_t)(bool enabled);
+
+struct retro_audio_callback
+{
+ retro_audio_callback_t callback;
+ retro_audio_set_state_callback_t set_state;
+};
+
+/* Notifies a libretro core of time spent since last invocation
+ * of retro_run() in microseconds.
+ *
+ * It will be called right before retro_run() every frame.
+ * The frontend can tamper with timing to support cases like
+ * fast-forward, slow-motion and framestepping.
+ *
+ * In those scenarios the reference frame time value will be used. */
+typedef int64_t retro_usec_t;
+typedef void (RETRO_CALLCONV *retro_frame_time_callback_t)(retro_usec_t usec);
+struct retro_frame_time_callback
+{
+ retro_frame_time_callback_t callback;
+ /* Represents the time of one frame. It is computed as
+ * 1000000 / fps, but the implementation will resolve the
+ * rounding to ensure that framestepping, etc is exact. */
+ retro_usec_t reference;
+};
+
+/* Notifies a libretro core of the current occupancy
+ * level of the frontend audio buffer.
+ *
+ * - active: 'true' if audio buffer is currently
+ * in use. Will be 'false' if audio is
+ * disabled in the frontend
+ *
+ * - occupancy: Given as a value in the range [0,100],
+ * corresponding to the occupancy percentage
+ * of the audio buffer
+ *
+ * - underrun_likely: 'true' if the frontend expects an
+ * audio buffer underrun during the
+ * next frame (indicates that a core
+ * should attempt frame skipping)
+ *
+ * It will be called right before retro_run() every frame. */
+typedef void (RETRO_CALLCONV *retro_audio_buffer_status_callback_t)(
+ bool active, unsigned occupancy, bool underrun_likely);
+struct retro_audio_buffer_status_callback
+{
+ retro_audio_buffer_status_callback_t callback;
+};
+
+/* Pass this to retro_video_refresh_t if rendering to hardware.
+ * Passing NULL to retro_video_refresh_t is still a frame dupe as normal.
+ * */
+#define RETRO_HW_FRAME_BUFFER_VALID ((void*)-1)
+
+/* Invalidates the current HW context.
+ * Any GL state is lost, and must not be deinitialized explicitly.
+ * If explicit deinitialization is desired by the libretro core,
+ * it should implement context_destroy callback.
+ * If called, all GPU resources must be reinitialized.
+ * Usually called when frontend reinits video driver.
+ * Also called first time video driver is initialized,
+ * allowing libretro core to initialize resources.
+ */
+typedef void (RETRO_CALLCONV *retro_hw_context_reset_t)(void);
+
+/* Gets current framebuffer which is to be rendered to.
+ * Could change every frame potentially.
+ */
+typedef uintptr_t (RETRO_CALLCONV *retro_hw_get_current_framebuffer_t)(void);
+
+/* Get a symbol from HW context. */
+typedef retro_proc_address_t (RETRO_CALLCONV *retro_hw_get_proc_address_t)(const char *sym);
+
+enum retro_hw_context_type
+{
+ RETRO_HW_CONTEXT_NONE = 0,
+ /* OpenGL 2.x. Driver can choose to use latest compatibility context. */
+ RETRO_HW_CONTEXT_OPENGL = 1,
+ /* OpenGL ES 2.0. */
+ RETRO_HW_CONTEXT_OPENGLES2 = 2,
+ /* Modern desktop core GL context. Use version_major/
+ * version_minor fields to set GL version. */
+ RETRO_HW_CONTEXT_OPENGL_CORE = 3,
+ /* OpenGL ES 3.0 */
+ RETRO_HW_CONTEXT_OPENGLES3 = 4,
+ /* OpenGL ES 3.1+. Set version_major/version_minor. For GLES2 and GLES3,
+ * use the corresponding enums directly. */
+ RETRO_HW_CONTEXT_OPENGLES_VERSION = 5,
+
+ /* Vulkan, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE. */
+ RETRO_HW_CONTEXT_VULKAN = 6,
+
+ /* Direct3D, set version_major to select the type of interface
+ * returned by RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */
+ RETRO_HW_CONTEXT_DIRECT3D = 7,
+
+ RETRO_HW_CONTEXT_DUMMY = INT_MAX
+};
+
+struct retro_hw_render_callback
+{
+ /* Which API to use. Set by libretro core. */
+ enum retro_hw_context_type context_type;
+
+ /* Called when a context has been created or when it has been reset.
+ * An OpenGL context is only valid after context_reset() has been called.
+ *
+ * When context_reset is called, OpenGL resources in the libretro
+ * implementation are guaranteed to be invalid.
+ *
+ * It is possible that context_reset is called multiple times during an
+ * application lifecycle.
+ * If context_reset is called without any notification (context_destroy),
+ * the OpenGL context was lost and resources should just be recreated
+ * without any attempt to "free" old resources.
+ */
+ retro_hw_context_reset_t context_reset;
+
+ /* Set by frontend.
+ * TODO: This is rather obsolete. The frontend should not
+ * be providing preallocated framebuffers. */
+ retro_hw_get_current_framebuffer_t get_current_framebuffer;
+
+ /* Set by frontend.
+ * Can return all relevant functions, including glClear on Windows. */
+ retro_hw_get_proc_address_t get_proc_address;
+
+ /* Set if render buffers should have depth component attached.
+ * TODO: Obsolete. */
+ bool depth;
+
+ /* Set if stencil buffers should be attached.
+ * TODO: Obsolete. */
+ bool stencil;
+
+ /* If depth and stencil are true, a packed 24/8 buffer will be added.
+ * Only attaching stencil is invalid and will be ignored. */
+
+ /* Use conventional bottom-left origin convention. If false,
+ * standard libretro top-left origin semantics are used.
+ * TODO: Move to GL specific interface. */
+ bool bottom_left_origin;
+
+ /* Major version number for core GL context or GLES 3.1+. */
+ unsigned version_major;
+
+ /* Minor version number for core GL context or GLES 3.1+. */
+ unsigned version_minor;
+
+ /* If this is true, the frontend will go very far to avoid
+ * resetting context in scenarios like toggling fullscreen, etc.
+ * TODO: Obsolete? Maybe frontend should just always assume this ...
+ */
+ bool cache_context;
+
+ /* The reset callback might still be called in extreme situations
+ * such as if the context is lost beyond recovery.
+ *
+ * For optimal stability, set this to false, and allow context to be
+ * reset at any time.
+ */
+
+ /* A callback to be called before the context is destroyed in a
+ * controlled way by the frontend. */
+ retro_hw_context_reset_t context_destroy;
+
+ /* OpenGL resources can be deinitialized cleanly at this step.
+ * context_destroy can be set to NULL, in which resources will
+ * just be destroyed without any notification.
+ *
+ * Even when context_destroy is non-NULL, it is possible that
+ * context_reset is called without any destroy notification.
+ * This happens if context is lost by external factors (such as
+ * notified by GL_ARB_robustness).
+ *
+ * In this case, the context is assumed to be already dead,
+ * and the libretro implementation must not try to free any OpenGL
+ * resources in the subsequent context_reset.
+ */
+
+ /* Creates a debug context. */
+ bool debug_context;
+};
+
+/* Callback type passed in RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK.
+ * Called by the frontend in response to keyboard events.
+ * down is set if the key is being pressed, or false if it is being released.
+ * keycode is the RETROK value of the char.
+ * character is the text character of the pressed key. (UTF-32).
+ * key_modifiers is a set of RETROKMOD values or'ed together.
+ *
+ * The pressed/keycode state can be indepedent of the character.
+ * It is also possible that multiple characters are generated from a
+ * single keypress.
+ * Keycode events should be treated separately from character events.
+ * However, when possible, the frontend should try to synchronize these.
+ * If only a character is posted, keycode should be RETROK_UNKNOWN.
+ *
+ * Similarily if only a keycode event is generated with no corresponding
+ * character, character should be 0.
+ */
+typedef void (RETRO_CALLCONV *retro_keyboard_event_t)(bool down, unsigned keycode,
+ uint32_t character, uint16_t key_modifiers);
+
+struct retro_keyboard_callback
+{
+ retro_keyboard_event_t callback;
+};
+
+/* Callbacks for RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE &
+ * RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE.
+ * Should be set for implementations which can swap out multiple disk
+ * images in runtime.
+ *
+ * If the implementation can do this automatically, it should strive to do so.
+ * However, there are cases where the user must manually do so.
+ *
+ * Overview: To swap a disk image, eject the disk image with
+ * set_eject_state(true).
+ * Set the disk index with set_image_index(index). Insert the disk again
+ * with set_eject_state(false).
+ */
+
+/* If ejected is true, "ejects" the virtual disk tray.
+ * When ejected, the disk image index can be set.
+ */
+typedef bool (RETRO_CALLCONV *retro_set_eject_state_t)(bool ejected);
+
+/* Gets current eject state. The initial state is 'not ejected'. */
+typedef bool (RETRO_CALLCONV *retro_get_eject_state_t)(void);
+
+/* Gets current disk index. First disk is index 0.
+ * If return value is >= get_num_images(), no disk is currently inserted.
+ */
+typedef unsigned (RETRO_CALLCONV *retro_get_image_index_t)(void);
+
+/* Sets image index. Can only be called when disk is ejected.
+ * The implementation supports setting "no disk" by using an
+ * index >= get_num_images().
+ */
+typedef bool (RETRO_CALLCONV *retro_set_image_index_t)(unsigned index);
+
+/* Gets total number of images which are available to use. */
+typedef unsigned (RETRO_CALLCONV *retro_get_num_images_t)(void);
+
+struct retro_game_info;
+
+/* Replaces the disk image associated with index.
+ * Arguments to pass in info have same requirements as retro_load_game().
+ * Virtual disk tray must be ejected when calling this.
+ *
+ * Replacing a disk image with info = NULL will remove the disk image
+ * from the internal list.
+ * As a result, calls to get_image_index() can change.
+ *
+ * E.g. replace_image_index(1, NULL), and previous get_image_index()
+ * returned 4 before.
+ * Index 1 will be removed, and the new index is 3.
+ */
+typedef bool (RETRO_CALLCONV *retro_replace_image_index_t)(unsigned index,
+ const struct retro_game_info *info);
+
+/* Adds a new valid index (get_num_images()) to the internal disk list.
+ * This will increment subsequent return values from get_num_images() by 1.
+ * This image index cannot be used until a disk image has been set
+ * with replace_image_index. */
+typedef bool (RETRO_CALLCONV *retro_add_image_index_t)(void);
+
+/* Sets initial image to insert in drive when calling
+ * core_load_game().
+ * Since we cannot pass the initial index when loading
+ * content (this would require a major API change), this
+ * is set by the frontend *before* calling the core's
+ * retro_load_game()/retro_load_game_special() implementation.
+ * A core should therefore cache the index/path values and handle
+ * them inside retro_load_game()/retro_load_game_special().
+ * - If 'index' is invalid (index >= get_num_images()), the
+ * core should ignore the set value and instead use 0
+ * - 'path' is used purely for error checking - i.e. when
+ * content is loaded, the core should verify that the
+ * disk specified by 'index' has the specified file path.
+ * This is to guard against auto selecting the wrong image
+ * if (for example) the user should modify an existing M3U
+ * playlist. We have to let the core handle this because
+ * set_initial_image() must be called before loading content,
+ * i.e. the frontend cannot access image paths in advance
+ * and thus cannot perform the error check itself.
+ * If set path and content path do not match, the core should
+ * ignore the set 'index' value and instead use 0
+ * Returns 'false' if index or 'path' are invalid, or core
+ * does not support this functionality
+ */
+typedef bool (RETRO_CALLCONV *retro_set_initial_image_t)(unsigned index, const char *path);
+
+/* Fetches the path of the specified disk image file.
+ * Returns 'false' if index is invalid (index >= get_num_images())
+ * or path is otherwise unavailable.
+ */
+typedef bool (RETRO_CALLCONV *retro_get_image_path_t)(unsigned index, char *path, size_t len);
+
+/* Fetches a core-provided 'label' for the specified disk
+ * image file. In the simplest case this may be a file name
+ * (without extension), but for cores with more complex
+ * content requirements information may be provided to
+ * facilitate user disk swapping - for example, a core
+ * running floppy-disk-based content may uniquely label
+ * save disks, data disks, level disks, etc. with names
+ * corresponding to in-game disk change prompts (so the
+ * frontend can provide better user guidance than a 'dumb'
+ * disk index value).
+ * Returns 'false' if index is invalid (index >= get_num_images())
+ * or label is otherwise unavailable.
+ */
+typedef bool (RETRO_CALLCONV *retro_get_image_label_t)(unsigned index, char *label, size_t len);
+
+struct retro_disk_control_callback
+{
+ retro_set_eject_state_t set_eject_state;
+ retro_get_eject_state_t get_eject_state;
+
+ retro_get_image_index_t get_image_index;
+ retro_set_image_index_t set_image_index;
+ retro_get_num_images_t get_num_images;
+
+ retro_replace_image_index_t replace_image_index;
+ retro_add_image_index_t add_image_index;
+};
+
+struct retro_disk_control_ext_callback
+{
+ retro_set_eject_state_t set_eject_state;
+ retro_get_eject_state_t get_eject_state;
+
+ retro_get_image_index_t get_image_index;
+ retro_set_image_index_t set_image_index;
+ retro_get_num_images_t get_num_images;
+
+ retro_replace_image_index_t replace_image_index;
+ retro_add_image_index_t add_image_index;
+
+ /* NOTE: Frontend will only attempt to record/restore
+ * last used disk index if both set_initial_image()
+ * and get_image_path() are implemented */
+ retro_set_initial_image_t set_initial_image; /* Optional - may be NULL */
+
+ retro_get_image_path_t get_image_path; /* Optional - may be NULL */
+ retro_get_image_label_t get_image_label; /* Optional - may be NULL */
+};
+
+enum retro_pixel_format
+{
+ /* 0RGB1555, native endian.
+ * 0 bit must be set to 0.
+ * This pixel format is default for compatibility concerns only.
+ * If a 15/16-bit pixel format is desired, consider using RGB565. */
+ RETRO_PIXEL_FORMAT_0RGB1555 = 0,
+
+ /* XRGB8888, native endian.
+ * X bits are ignored. */
+ RETRO_PIXEL_FORMAT_XRGB8888 = 1,
+
+ /* RGB565, native endian.
+ * This pixel format is the recommended format to use if a 15/16-bit
+ * format is desired as it is the pixel format that is typically
+ * available on a wide range of low-power devices.
+ *
+ * It is also natively supported in APIs like OpenGL ES. */
+ RETRO_PIXEL_FORMAT_RGB565 = 2,
+
+ /* Ensure sizeof() == sizeof(int). */
+ RETRO_PIXEL_FORMAT_UNKNOWN = INT_MAX
+};
+
+enum retro_savestate_context
+{
+ /* Standard savestate written to disk. */
+ RETRO_SAVESTATE_CONTEXT_NORMAL = 0,
+
+ /* Savestate where you are guaranteed that the same instance will load the save state.
+ * You can store internal pointers to code or data.
+ * It's still a full serialization and deserialization, and could be loaded or saved at any time.
+ * It won't be written to disk or sent over the network.
+ */
+ RETRO_SAVESTATE_CONTEXT_RUNAHEAD_SAME_INSTANCE = 1,
+
+ /* Savestate where you are guaranteed that the same emulator binary will load that savestate.
+ * You can skip anything that would slow down saving or loading state but you can not store internal pointers.
+ * It won't be written to disk or sent over the network.
+ * Example: "Second Instance" runahead
+ */
+ RETRO_SAVESTATE_CONTEXT_RUNAHEAD_SAME_BINARY = 2,
+
+ /* Savestate used within a rollback netplay feature.
+ * You should skip anything that would unnecessarily increase bandwidth usage.
+ * It won't be written to disk but it will be sent over the network.
+ */
+ RETRO_SAVESTATE_CONTEXT_ROLLBACK_NETPLAY = 3,
+
+ /* Ensure sizeof() == sizeof(int). */
+ RETRO_SAVESTATE_CONTEXT_UNKNOWN = INT_MAX
+};
+
+struct retro_message
+{
+ const char *msg; /* Message to be displayed. */
+ unsigned frames; /* Duration in frames of message. */
+};
+
+enum retro_message_target
+{
+ RETRO_MESSAGE_TARGET_ALL = 0,
+ RETRO_MESSAGE_TARGET_OSD,
+ RETRO_MESSAGE_TARGET_LOG
+};
+
+enum retro_message_type
+{
+ RETRO_MESSAGE_TYPE_NOTIFICATION = 0,
+ RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
+ RETRO_MESSAGE_TYPE_STATUS,
+ RETRO_MESSAGE_TYPE_PROGRESS
+};
+
+struct retro_message_ext
+{
+ /* Message string to be displayed/logged */
+ const char *msg;
+ /* Duration (in ms) of message when targeting the OSD */
+ unsigned duration;
+ /* Message priority when targeting the OSD
+ * > When multiple concurrent messages are sent to
+ * the frontend and the frontend does not have the
+ * capacity to display them all, messages with the
+ * *highest* priority value should be shown
+ * > There is no upper limit to a message priority
+ * value (within the bounds of the unsigned data type)
+ * > In the reference frontend (RetroArch), the same
+ * priority values are used for frontend-generated
+ * notifications, which are typically assigned values
+ * between 0 and 3 depending upon importance */
+ unsigned priority;
+ /* Message logging level (info, warn, error, etc.) */
+ enum retro_log_level level;
+ /* Message destination: OSD, logging interface or both */
+ enum retro_message_target target;
+ /* Message 'type' when targeting the OSD
+ * > RETRO_MESSAGE_TYPE_NOTIFICATION: Specifies that a
+ * message should be handled in identical fashion to
+ * a standard frontend-generated notification
+ * > RETRO_MESSAGE_TYPE_NOTIFICATION_ALT: Specifies that
+ * message is a notification that requires user attention
+ * or action, but that it should be displayed in a manner
+ * that differs from standard frontend-generated notifications.
+ * This would typically correspond to messages that should be
+ * displayed immediately (independently from any internal
+ * frontend message queue), and/or which should be visually
+ * distinguishable from frontend-generated notifications.
+ * For example, a core may wish to inform the user of
+ * information related to a disk-change event. It is
+ * expected that the frontend itself may provide a
+ * notification in this case; if the core sends a
+ * message of type RETRO_MESSAGE_TYPE_NOTIFICATION, an
+ * uncomfortable 'double-notification' may occur. A message
+ * of RETRO_MESSAGE_TYPE_NOTIFICATION_ALT should therefore
+ * be presented such that visual conflict with regular
+ * notifications does not occur
+ * > RETRO_MESSAGE_TYPE_STATUS: Indicates that message
+ * is not a standard notification. This typically
+ * corresponds to 'status' indicators, such as a core's
+ * internal FPS, which are intended to be displayed
+ * either permanently while a core is running, or in
+ * a manner that does not suggest user attention or action
+ * is required. 'Status' type messages should therefore be
+ * displayed in a different on-screen location and in a manner
+ * easily distinguishable from both standard frontend-generated
+ * notifications and messages of type RETRO_MESSAGE_TYPE_NOTIFICATION_ALT
+ * > RETRO_MESSAGE_TYPE_PROGRESS: Indicates that message reports
+ * the progress of an internal core task. For example, in cases
+ * where a core itself handles the loading of content from a file,
+ * this may correspond to the percentage of the file that has been
+ * read. Alternatively, an audio/video playback core may use a
+ * message of type RETRO_MESSAGE_TYPE_PROGRESS to display the current
+ * playback position as a percentage of the runtime. 'Progress' type
+ * messages should therefore be displayed as a literal progress bar,
+ * where:
+ * - 'retro_message_ext.msg' is the progress bar title/label
+ * - 'retro_message_ext.progress' determines the length of
+ * the progress bar
+ * NOTE: Message type is a *hint*, and may be ignored
+ * by the frontend. If a frontend lacks support for
+ * displaying messages via alternate means than standard
+ * frontend-generated notifications, it will treat *all*
+ * messages as having the type RETRO_MESSAGE_TYPE_NOTIFICATION */
+ enum retro_message_type type;
+ /* Task progress when targeting the OSD and message is
+ * of type RETRO_MESSAGE_TYPE_PROGRESS
+ * > -1: Unmetered/indeterminate
+ * > 0-100: Current progress percentage
+ * NOTE: Since message type is a hint, a frontend may ignore
+ * progress values. Where relevant, a core should therefore
+ * include progress percentage within the message string,
+ * such that the message intent remains clear when displayed
+ * as a standard frontend-generated notification */
+ int8_t progress;
+};
+
+/* Describes how the libretro implementation maps a libretro input bind
+ * to its internal input system through a human readable string.
+ * This string can be used to better let a user configure input. */
+struct retro_input_descriptor
+{
+ /* Associates given parameters with a description. */
+ unsigned port;
+ unsigned device;
+ unsigned index;
+ unsigned id;
+
+ /* Human readable description for parameters.
+ * The pointer must remain valid until
+ * retro_unload_game() is called. */
+ const char *description;
+};
+
+struct retro_system_info
+{
+ /* All pointers are owned by libretro implementation, and pointers must
+ * remain valid until it is unloaded. */
+
+ const char *library_name; /* Descriptive name of library. Should not
+ * contain any version numbers, etc. */
+ const char *library_version; /* Descriptive version of core. */
+
+ const char *valid_extensions; /* A string listing probably content
+ * extensions the core will be able to
+ * load, separated with pipe.
+ * I.e. "bin|rom|iso".
+ * Typically used for a GUI to filter
+ * out extensions. */
+
+ /* Libretro cores that need to have direct access to their content
+ * files, including cores which use the path of the content files to
+ * determine the paths of other files, should set need_fullpath to true.
+ *
+ * Cores should strive for setting need_fullpath to false,
+ * as it allows the frontend to perform patching, etc.
+ *
+ * If need_fullpath is true and retro_load_game() is called:
+ * - retro_game_info::path is guaranteed to have a valid path
+ * - retro_game_info::data and retro_game_info::size are invalid
+ *
+ * If need_fullpath is false and retro_load_game() is called:
+ * - retro_game_info::path may be NULL
+ * - retro_game_info::data and retro_game_info::size are guaranteed
+ * to be valid
+ *
+ * See also:
+ * - RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY
+ * - RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY
+ */
+ bool need_fullpath;
+
+ /* If true, the frontend is not allowed to extract any archives before
+ * loading the real content.
+ * Necessary for certain libretro implementations that load games
+ * from zipped archives. */
+ bool block_extract;
+};
+
+/* Defines overrides which modify frontend handling of
+ * specific content file types.
+ * An array of retro_system_content_info_override is
+ * passed to RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE
+ * NOTE: In the following descriptions, references to
+ * retro_load_game() may be replaced with
+ * retro_load_game_special() */
+struct retro_system_content_info_override
+{
+ /* A list of file extensions for which the override
+ * should apply, delimited by a 'pipe' character
+ * (e.g. "md|sms|gg")
+ * Permitted file extensions are limited to those
+ * included in retro_system_info::valid_extensions
+ * and/or retro_subsystem_rom_info::valid_extensions */
+ const char *extensions;
+
+ /* Overrides the need_fullpath value set in
+ * retro_system_info and/or retro_subsystem_rom_info.
+ * To reiterate:
+ *
+ * If need_fullpath is true and retro_load_game() is called:
+ * - retro_game_info::path is guaranteed to contain a valid
+ * path to an existent file
+ * - retro_game_info::data and retro_game_info::size are invalid
+ *
+ * If need_fullpath is false and retro_load_game() is called:
+ * - retro_game_info::path may be NULL
+ * - retro_game_info::data and retro_game_info::size are guaranteed
+ * to be valid
+ *
+ * In addition:
+ *
+ * If need_fullpath is true and retro_load_game() is called:
+ * - retro_game_info_ext::full_path is guaranteed to contain a valid
+ * path to an existent file
+ * - retro_game_info_ext::archive_path may be NULL
+ * - retro_game_info_ext::archive_file may be NULL
+ * - retro_game_info_ext::dir is guaranteed to contain a valid path
+ * to the directory in which the content file exists
+ * - retro_game_info_ext::name is guaranteed to contain the
+ * basename of the content file, without extension
+ * - retro_game_info_ext::ext is guaranteed to contain the
+ * extension of the content file in lower case format
+ * - retro_game_info_ext::data and retro_game_info_ext::size
+ * are invalid
+ *
+ * If need_fullpath is false and retro_load_game() is called:
+ * - If retro_game_info_ext::file_in_archive is false:
+ * - retro_game_info_ext::full_path is guaranteed to contain
+ * a valid path to an existent file
+ * - retro_game_info_ext::archive_path may be NULL
+ * - retro_game_info_ext::archive_file may be NULL
+ * - retro_game_info_ext::dir is guaranteed to contain a
+ * valid path to the directory in which the content file exists
+ * - retro_game_info_ext::name is guaranteed to contain the
+ * basename of the content file, without extension
+ * - retro_game_info_ext::ext is guaranteed to contain the
+ * extension of the content file in lower case format
+ * - If retro_game_info_ext::file_in_archive is true:
+ * - retro_game_info_ext::full_path may be NULL
+ * - retro_game_info_ext::archive_path is guaranteed to
+ * contain a valid path to an existent compressed file
+ * inside which the content file is located
+ * - retro_game_info_ext::archive_file is guaranteed to
+ * contain a valid path to an existent content file
+ * inside the compressed file referred to by
+ * retro_game_info_ext::archive_path
+ * e.g. for a compressed file '/path/to/foo.zip'
+ * containing 'bar.sfc'
+ * > retro_game_info_ext::archive_path will be '/path/to/foo.zip'
+ * > retro_game_info_ext::archive_file will be 'bar.sfc'
+ * - retro_game_info_ext::dir is guaranteed to contain a
+ * valid path to the directory in which the compressed file
+ * (containing the content file) exists
+ * - retro_game_info_ext::name is guaranteed to contain
+ * EITHER
+ * 1) the basename of the compressed file (containing
+ * the content file), without extension
+ * OR
+ * 2) the basename of the content file inside the
+ * compressed file, without extension
+ * In either case, a core should consider 'name' to
+ * be the canonical name/ID of the the content file
+ * - retro_game_info_ext::ext is guaranteed to contain the
+ * extension of the content file inside the compressed file,
+ * in lower case format
+ * - retro_game_info_ext::data and retro_game_info_ext::size are
+ * guaranteed to be valid */
+ bool need_fullpath;
+
+ /* If need_fullpath is false, specifies whether the content
+ * data buffer available in retro_load_game() is 'persistent'
+ *
+ * If persistent_data is false and retro_load_game() is called:
+ * - retro_game_info::data and retro_game_info::size
+ * are valid only until retro_load_game() returns
+ * - retro_game_info_ext::data and retro_game_info_ext::size
+ * are valid only until retro_load_game() returns
+ *
+ * If persistent_data is true and retro_load_game() is called:
+ * - retro_game_info::data and retro_game_info::size
+ * are valid until retro_deinit() returns
+ * - retro_game_info_ext::data and retro_game_info_ext::size
+ * are valid until retro_deinit() returns */
+ bool persistent_data;
+};
+
+/* Similar to retro_game_info, but provides extended
+ * information about the source content file and
+ * game memory buffer status.
+ * And array of retro_game_info_ext is returned by
+ * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT
+ * NOTE: In the following descriptions, references to
+ * retro_load_game() may be replaced with
+ * retro_load_game_special() */
+struct retro_game_info_ext
+{
+ /* - If file_in_archive is false, contains a valid
+ * path to an existent content file (UTF-8 encoded)
+ * - If file_in_archive is true, may be NULL */
+ const char *full_path;
+
+ /* - If file_in_archive is false, may be NULL
+ * - If file_in_archive is true, contains a valid path
+ * to an existent compressed file inside which the
+ * content file is located (UTF-8 encoded) */
+ const char *archive_path;
+
+ /* - If file_in_archive is false, may be NULL
+ * - If file_in_archive is true, contain a valid path
+ * to an existent content file inside the compressed
+ * file referred to by archive_path (UTF-8 encoded)
+ * e.g. for a compressed file '/path/to/foo.zip'
+ * containing 'bar.sfc'
+ * > archive_path will be '/path/to/foo.zip'
+ * > archive_file will be 'bar.sfc' */
+ const char *archive_file;
+
+ /* - If file_in_archive is false, contains a valid path
+ * to the directory in which the content file exists
+ * (UTF-8 encoded)
+ * - If file_in_archive is true, contains a valid path
+ * to the directory in which the compressed file
+ * (containing the content file) exists (UTF-8 encoded) */
+ const char *dir;
+
+ /* Contains the canonical name/ID of the content file
+ * (UTF-8 encoded). Intended for use when identifying
+ * 'complementary' content named after the loaded file -
+ * i.e. companion data of a different format (a CD image
+ * required by a ROM), texture packs, internally handled
+ * save files, etc.
+ * - If file_in_archive is false, contains the basename
+ * of the content file, without extension
+ * - If file_in_archive is true, then string is
+ * implementation specific. A frontend may choose to
+ * set a name value of:
+ * EITHER
+ * 1) the basename of the compressed file (containing
+ * the content file), without extension
+ * OR
+ * 2) the basename of the content file inside the
+ * compressed file, without extension
+ * RetroArch sets the 'name' value according to (1).
+ * A frontend that supports routine loading of
+ * content from archives containing multiple unrelated
+ * content files may set the 'name' value according
+ * to (2). */
+ const char *name;
+
+ /* - If file_in_archive is false, contains the extension
+ * of the content file in lower case format
+ * - If file_in_archive is true, contains the extension
+ * of the content file inside the compressed file,
+ * in lower case format */
+ const char *ext;
+
+ /* String of implementation specific meta-data. */
+ const char *meta;
+
+ /* Memory buffer of loaded game content. Will be NULL:
+ * IF
+ * - retro_system_info::need_fullpath is true and
+ * retro_system_content_info_override::need_fullpath
+ * is unset
+ * OR
+ * - retro_system_content_info_override::need_fullpath
+ * is true */
+ const void *data;
+
+ /* Size of game content memory buffer, in bytes */
+ size_t size;
+
+ /* True if loaded content file is inside a compressed
+ * archive */
+ bool file_in_archive;
+
+ /* - If data is NULL, value is unset/ignored
+ * - If data is non-NULL:
+ * - If persistent_data is false, data and size are
+ * valid only until retro_load_game() returns
+ * - If persistent_data is true, data and size are
+ * are valid until retro_deinit() returns */
+ bool persistent_data;
+};
+
+struct retro_game_geometry
+{
+ unsigned base_width; /* Nominal video width of game. */
+ unsigned base_height; /* Nominal video height of game. */
+ unsigned max_width; /* Maximum possible width of game. */
+ unsigned max_height; /* Maximum possible height of game. */
+
+ float aspect_ratio; /* Nominal aspect ratio of game. If
+ * aspect_ratio is <= 0.0, an aspect ratio
+ * of base_width / base_height is assumed.
+ * A frontend could override this setting,
+ * if desired. */
+};
+
+struct retro_system_timing
+{
+ double fps; /* FPS of video content. */
+ double sample_rate; /* Sampling rate of audio. */
+};
+
+struct retro_system_av_info
+{
+ struct retro_game_geometry geometry;
+ struct retro_system_timing timing;
+};
+
+struct retro_variable
+{
+ /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE.
+ * If NULL, obtains the complete environment string if more
+ * complex parsing is necessary.
+ * The environment string is formatted as key-value pairs
+ * delimited by semicolons as so:
+ * "key1=value1;key2=value2;..."
+ */
+ const char *key;
+
+ /* Value to be obtained. If key does not exist, it is set to NULL. */
+ const char *value;
+};
+
+struct retro_core_option_display
+{
+ /* Variable to configure in RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY */
+ const char *key;
+
+ /* Specifies whether variable should be displayed
+ * when presenting core options to the user */
+ bool visible;
+};
+
+/* Maximum number of values permitted for a core option
+ * > Note: We have to set a maximum value due the limitations
+ * of the C language - i.e. it is not possible to create an
+ * array of structs each containing a variable sized array,
+ * so the retro_core_option_definition values array must
+ * have a fixed size. The size limit of 128 is a balancing
+ * act - it needs to be large enough to support all 'sane'
+ * core options, but setting it too large may impact low memory
+ * platforms. In practise, if a core option has more than
+ * 128 values then the implementation is likely flawed.
+ * To quote the above API reference:
+ * "The number of possible options should be very limited
+ * i.e. it should be feasible to cycle through options
+ * without a keyboard."
+ */
+#define RETRO_NUM_CORE_OPTION_VALUES_MAX 128
+
+struct retro_core_option_value
+{
+ /* Expected option value */
+ const char *value;
+
+ /* Human-readable value label. If NULL, value itself
+ * will be displayed by the frontend */
+ const char *label;
+};
+
+struct retro_core_option_definition
+{
+ /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. */
+ const char *key;
+
+ /* Human-readable core option description (used as menu label) */
+ const char *desc;
+
+ /* Human-readable core option information (used as menu sublabel) */
+ const char *info;
+
+ /* Array of retro_core_option_value structs, terminated by NULL */
+ struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX];
+
+ /* Default core option value. Must match one of the values
+ * in the retro_core_option_value array, otherwise will be
+ * ignored */
+ const char *default_value;
+};
+
+#ifdef __PS3__
+#undef local
+#endif
+
+struct retro_core_options_intl
+{
+ /* Pointer to an array of retro_core_option_definition structs
+ * - US English implementation
+ * - Must point to a valid array */
+ struct retro_core_option_definition *us;
+
+ /* Pointer to an array of retro_core_option_definition structs
+ * - Implementation for current frontend language
+ * - May be NULL */
+ struct retro_core_option_definition *local;
+};
+
+struct retro_core_option_v2_category
+{
+ /* Variable uniquely identifying the
+ * option category. Valid key characters
+ * are [a-z, A-Z, 0-9, _, -] */
+ const char *key;
+
+ /* Human-readable category description
+ * > Used as category menu label when
+ * frontend has core option category
+ * support */
+ const char *desc;
+
+ /* Human-readable category information
+ * > Used as category menu sublabel when
+ * frontend has core option category
+ * support
+ * > Optional (may be NULL or an empty
+ * string) */
+ const char *info;
+};
+
+struct retro_core_option_v2_definition
+{
+ /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE.
+ * Valid key characters are [a-z, A-Z, 0-9, _, -] */
+ const char *key;
+
+ /* Human-readable core option description
+ * > Used as menu label when frontend does
+ * not have core option category support
+ * e.g. "Video > Aspect Ratio" */
+ const char *desc;
+
+ /* Human-readable core option description
+ * > Used as menu label when frontend has
+ * core option category support
+ * e.g. "Aspect Ratio", where associated
+ * retro_core_option_v2_category::desc
+ * is "Video"
+ * > If empty or NULL, the string specified by
+ * desc will be used as the menu label
+ * > Will be ignored (and may be set to NULL)
+ * if category_key is empty or NULL */
+ const char *desc_categorized;
+
+ /* Human-readable core option information
+ * > Used as menu sublabel */
+ const char *info;
+
+ /* Human-readable core option information
+ * > Used as menu sublabel when frontend
+ * has core option category support
+ * (e.g. may be required when info text
+ * references an option by name/desc,
+ * and the desc/desc_categorized text
+ * for that option differ)
+ * > If empty or NULL, the string specified by
+ * info will be used as the menu sublabel
+ * > Will be ignored (and may be set to NULL)
+ * if category_key is empty or NULL */
+ const char *info_categorized;
+
+ /* Variable specifying category (e.g. "video",
+ * "audio") that will be assigned to the option
+ * if frontend has core option category support.
+ * > Categorized options will be displayed in a
+ * subsection/submenu of the frontend core
+ * option interface
+ * > Specified string must match one of the
+ * retro_core_option_v2_category::key values
+ * in the associated retro_core_option_v2_category
+ * array; If no match is not found, specified
+ * string will be considered as NULL
+ * > If specified string is empty or NULL, option will
+ * have no category and will be shown at the top
+ * level of the frontend core option interface */
+ const char *category_key;
+
+ /* Array of retro_core_option_value structs, terminated by NULL */
+ struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX];
+
+ /* Default core option value. Must match one of the values
+ * in the retro_core_option_value array, otherwise will be
+ * ignored */
+ const char *default_value;
+};
+
+struct retro_core_options_v2
+{
+ /* Array of retro_core_option_v2_category structs,
+ * terminated by NULL
+ * > If NULL, all entries in definitions array
+ * will have no category and will be shown at
+ * the top level of the frontend core option
+ * interface
+ * > Will be ignored if frontend does not have
+ * core option category support */
+ struct retro_core_option_v2_category *categories;
+
+ /* Array of retro_core_option_v2_definition structs,
+ * terminated by NULL */
+ struct retro_core_option_v2_definition *definitions;
+};
+
+struct retro_core_options_v2_intl
+{
+ /* Pointer to a retro_core_options_v2 struct
+ * > US English implementation
+ * > Must point to a valid struct */
+ struct retro_core_options_v2 *us;
+
+ /* Pointer to a retro_core_options_v2 struct
+ * - Implementation for current frontend language
+ * - May be NULL */
+ struct retro_core_options_v2 *local;
+};
+
+/* Used by the frontend to monitor changes in core option
+ * visibility. May be called each time any core option
+ * value is set via the frontend.
+ * - On each invocation, the core must update the visibility
+ * of any dynamically hidden options using the
+ * RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY environment
+ * callback.
+ * - On the first invocation, returns 'true' if the visibility
+ * of any core option has changed since the last call of
+ * retro_load_game() or retro_load_game_special().
+ * - On each subsequent invocation, returns 'true' if the
+ * visibility of any core option has changed since the last
+ * time the function was called. */
+typedef bool (RETRO_CALLCONV *retro_core_options_update_display_callback_t)(void);
+struct retro_core_options_update_display_callback
+{
+ retro_core_options_update_display_callback_t callback;
+};
+
+struct retro_game_info
+{
+ const char *path; /* Path to game, UTF-8 encoded.
+ * Sometimes used as a reference for building other paths.
+ * May be NULL if game was loaded from stdin or similar,
+ * but in this case some cores will be unable to load `data`.
+ * So, it is preferable to fabricate something here instead
+ * of passing NULL, which will help more cores to succeed.
+ * retro_system_info::need_fullpath requires
+ * that this path is valid. */
+ const void *data; /* Memory buffer of loaded game. Will be NULL
+ * if need_fullpath was set. */
+ size_t size; /* Size of memory buffer. */
+ const char *meta; /* String of implementation specific meta-data. */
+};
+
+#define RETRO_MEMORY_ACCESS_WRITE (1 << 0)
+ /* The core will write to the buffer provided by retro_framebuffer::data. */
+#define RETRO_MEMORY_ACCESS_READ (1 << 1)
+ /* The core will read from retro_framebuffer::data. */
+#define RETRO_MEMORY_TYPE_CACHED (1 << 0)
+ /* The memory in data is cached.
+ * If not cached, random writes and/or reading from the buffer is expected to be very slow. */
+struct retro_framebuffer
+{
+ void *data; /* The framebuffer which the core can render into.
+ Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER.
+ The initial contents of data are unspecified. */
+ unsigned width; /* The framebuffer width used by the core. Set by core. */
+ unsigned height; /* The framebuffer height used by the core. Set by core. */
+ size_t pitch; /* The number of bytes between the beginning of a scanline,
+ and beginning of the next scanline.
+ Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */
+ enum retro_pixel_format format; /* The pixel format the core must use to render into data.
+ This format could differ from the format used in
+ SET_PIXEL_FORMAT.
+ Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */
+
+ unsigned access_flags; /* How the core will access the memory in the framebuffer.
+ RETRO_MEMORY_ACCESS_* flags.
+ Set by core. */
+ unsigned memory_flags; /* Flags telling core how the memory has been mapped.
+ RETRO_MEMORY_TYPE_* flags.
+ Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */
+};
+
+/* Used by a libretro core to override the current
+ * fastforwarding mode of the frontend */
+struct retro_fastforwarding_override
+{
+ /* Specifies the runtime speed multiplier that
+ * will be applied when 'fastforward' is true.
+ * For example, a value of 5.0 when running 60 FPS
+ * content will cap the fast-forward rate at 300 FPS.
+ * Note that the target multiplier may not be achieved
+ * if the host hardware has insufficient processing
+ * power.
+ * Setting a value of 0.0 (or greater than 0.0 but
+ * less than 1.0) will result in an uncapped
+ * fast-forward rate (limited only by hardware
+ * capacity).
+ * If the value is negative, it will be ignored
+ * (i.e. the frontend will use a runtime speed
+ * multiplier of its own choosing) */
+ float ratio;
+
+ /* If true, fastforwarding mode will be enabled.
+ * If false, fastforwarding mode will be disabled. */
+ bool fastforward;
+
+ /* If true, and if supported by the frontend, an
+ * on-screen notification will be displayed while
+ * 'fastforward' is true.
+ * If false, and if supported by the frontend, any
+ * on-screen fast-forward notifications will be
+ * suppressed */
+ bool notification;
+
+ /* If true, the core will have sole control over
+ * when fastforwarding mode is enabled/disabled;
+ * the frontend will not be able to change the
+ * state set by 'fastforward' until either
+ * 'inhibit_toggle' is set to false, or the core
+ * is unloaded */
+ bool inhibit_toggle;
+};
+
+/* During normal operation. Rate will be equal to the core's internal FPS. */
+#define RETRO_THROTTLE_NONE 0
+
+/* While paused or stepping single frames. Rate will be 0. */
+#define RETRO_THROTTLE_FRAME_STEPPING 1
+
+/* During fast forwarding.
+ * Rate will be 0 if not specifically limited to a maximum speed. */
+#define RETRO_THROTTLE_FAST_FORWARD 2
+
+/* During slow motion. Rate will be less than the core's internal FPS. */
+#define RETRO_THROTTLE_SLOW_MOTION 3
+
+/* While rewinding recorded save states. Rate can vary depending on the rewind
+ * speed or be 0 if the frontend is not aiming for a specific rate. */
+#define RETRO_THROTTLE_REWINDING 4
+
+/* While vsync is active in the video driver and the target refresh rate is
+ * lower than the core's internal FPS. Rate is the target refresh rate. */
+#define RETRO_THROTTLE_VSYNC 5
+
+/* When the frontend does not throttle in any way. Rate will be 0.
+ * An example could be if no vsync or audio output is active. */
+#define RETRO_THROTTLE_UNBLOCKED 6
+
+struct retro_throttle_state
+{
+ /* The current throttling mode. Should be one of the values above. */
+ unsigned mode;
+
+ /* How many times per second the frontend aims to call retro_run.
+ * Depending on the mode, it can be 0 if there is no known fixed rate.
+ * This won't be accurate if the total processing time of the core and
+ * the frontend is longer than what is available for one frame. */
+ float rate;
+};
+
+/* Callbacks */
+
+/* Environment callback. Gives implementations a way of performing
+ * uncommon tasks. Extensible. */
+typedef bool (RETRO_CALLCONV *retro_environment_t)(unsigned cmd, void *data);
+
+/* Render a frame. Pixel format is 15-bit 0RGB1555 native endian
+ * unless changed (see RETRO_ENVIRONMENT_SET_PIXEL_FORMAT).
+ *
+ * Width and height specify dimensions of buffer.
+ * Pitch specifices length in bytes between two lines in buffer.
+ *
+ * For performance reasons, it is highly recommended to have a frame
+ * that is packed in memory, i.e. pitch == width * byte_per_pixel.
+ * Certain graphic APIs, such as OpenGL ES, do not like textures
+ * that are not packed in memory.
+ */
+typedef void (RETRO_CALLCONV *retro_video_refresh_t)(const void *data, unsigned width,
+ unsigned height, size_t pitch);
+
+/* Renders a single audio frame. Should only be used if implementation
+ * generates a single sample at a time.
+ * Format is signed 16-bit native endian.
+ */
+typedef void (RETRO_CALLCONV *retro_audio_sample_t)(int16_t left, int16_t right);
+
+/* Renders multiple audio frames in one go.
+ *
+ * One frame is defined as a sample of left and right channels, interleaved.
+ * I.e. int16_t buf[4] = { l, r, l, r }; would be 2 frames.
+ * Only one of the audio callbacks must ever be used.
+ */
+typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_t)(const int16_t *data,
+ size_t frames);
+
+/* Polls input. */
+typedef void (RETRO_CALLCONV *retro_input_poll_t)(void);
+
+/* Queries for input for player 'port'. device will be masked with
+ * RETRO_DEVICE_MASK.
+ *
+ * Specialization of devices such as RETRO_DEVICE_JOYPAD_MULTITAP that
+ * have been set with retro_set_controller_port_device()
+ * will still use the higher level RETRO_DEVICE_JOYPAD to request input.
+ */
+typedef int16_t (RETRO_CALLCONV *retro_input_state_t)(unsigned port, unsigned device,
+ unsigned index, unsigned id);
+
+/* Sets callbacks. retro_set_environment() is guaranteed to be called
+ * before retro_init().
+ *
+ * The rest of the set_* functions are guaranteed to have been called
+ * before the first call to retro_run() is made. */
+RETRO_API void retro_set_environment(retro_environment_t);
+RETRO_API void retro_set_video_refresh(retro_video_refresh_t);
+RETRO_API void retro_set_audio_sample(retro_audio_sample_t);
+RETRO_API void retro_set_audio_sample_batch(retro_audio_sample_batch_t);
+RETRO_API void retro_set_input_poll(retro_input_poll_t);
+RETRO_API void retro_set_input_state(retro_input_state_t);
+
+/* Library global initialization/deinitialization. */
+RETRO_API void retro_init(void);
+RETRO_API void retro_deinit(void);
+
+/* Must return RETRO_API_VERSION. Used to validate ABI compatibility
+ * when the API is revised. */
+RETRO_API unsigned retro_api_version(void);
+
+/* Gets statically known system info. Pointers provided in *info
+ * must be statically allocated.
+ * Can be called at any time, even before retro_init(). */
+RETRO_API void retro_get_system_info(struct retro_system_info *info);
+
+/* Gets information about system audio/video timings and geometry.
+ * Can be called only after retro_load_game() has successfully completed.
+ * NOTE: The implementation of this function might not initialize every
+ * variable if needed.
+ * E.g. geom.aspect_ratio might not be initialized if core doesn't
+ * desire a particular aspect ratio. */
+RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info);
+
+/* Sets device to be used for player 'port'.
+ * By default, RETRO_DEVICE_JOYPAD is assumed to be plugged into all
+ * available ports.
+ * Setting a particular device type is not a guarantee that libretro cores
+ * will only poll input based on that particular device type. It is only a
+ * hint to the libretro core when a core cannot automatically detect the
+ * appropriate input device type on its own. It is also relevant when a
+ * core can change its behavior depending on device type.
+ *
+ * As part of the core's implementation of retro_set_controller_port_device,
+ * the core should call RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS to notify the
+ * frontend if the descriptions for any controls have changed as a
+ * result of changing the device type.
+ */
+RETRO_API void retro_set_controller_port_device(unsigned port, unsigned device);
+
+/* Resets the current game. */
+RETRO_API void retro_reset(void);
+
+/* Runs the game for one video frame.
+ * During retro_run(), input_poll callback must be called at least once.
+ *
+ * If a frame is not rendered for reasons where a game "dropped" a frame,
+ * this still counts as a frame, and retro_run() should explicitly dupe
+ * a frame if GET_CAN_DUPE returns true.
+ * In this case, the video callback can take a NULL argument for data.
+ */
+RETRO_API void retro_run(void);
+
+/* Returns the amount of data the implementation requires to serialize
+ * internal state (save states).
+ * Between calls to retro_load_game() and retro_unload_game(), the
+ * returned size is never allowed to be larger than a previous returned
+ * value, to ensure that the frontend can allocate a save state buffer once.
+ */
+RETRO_API size_t retro_serialize_size(void);
+
+/* Serializes internal state. If failed, or size is lower than
+ * retro_serialize_size(), it should return false, true otherwise. */
+RETRO_API bool retro_serialize(void *data, size_t size);
+RETRO_API bool retro_unserialize(const void *data, size_t size);
+
+RETRO_API void retro_cheat_reset(void);
+RETRO_API void retro_cheat_set(unsigned index, bool enabled, const char *code);
+
+/* Loads a game.
+ * Return true to indicate successful loading and false to indicate load failure.
+ */
+RETRO_API bool retro_load_game(const struct retro_game_info *game);
+
+/* Loads a "special" kind of game. Should not be used,
+ * except in extreme cases. */
+RETRO_API bool retro_load_game_special(
+ unsigned game_type,
+ const struct retro_game_info *info, size_t num_info
+);
+
+/* Unloads the currently loaded game. Called before retro_deinit(void). */
+RETRO_API void retro_unload_game(void);
+
+/* Gets region of game. */
+RETRO_API unsigned retro_get_region(void);
+
+/* Gets region of memory. */
+RETRO_API void *retro_get_memory_data(unsigned id);
+RETRO_API size_t retro_get_memory_size(unsigned id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/libretro/libretro_vulkan.h b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/libretro/libretro_vulkan.h
new file mode 100644
index 00000000..bfc93af5
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/libretro/libretro_vulkan.h
@@ -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
+#include
+
+#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
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/scene_viewer_application.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/scene_viewer_application.cpp
new file mode 100644
index 00000000..01f95754
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/scene_viewer_application.cpp
@@ -0,0 +1,1618 @@
+/* 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 "scene_viewer_application.hpp"
+#include "light_export.hpp"
+#include "muglm/matrix_helper.hpp"
+#include "post/hdr.hpp"
+#include "post/ssao.hpp"
+#include "post/spd.hpp"
+#include "rapidjson_wrapper.hpp"
+#include "task_composer.hpp"
+#include "thread_group.hpp"
+#include "utils/image_utils.hpp"
+#include "ocean.hpp"
+#include "post/ssr.hpp"
+#include
+#include
+
+using namespace Vulkan;
+
+namespace Granite
+{
+
+static vec3 light_direction()
+{
+ return normalize(vec3(0.5f, 1.2f, 0.8f));
+}
+
+void SceneViewerApplication::read_lights()
+{
+ std::string json;
+ if (!GRANITE_FILESYSTEM()->read_file_to_string("assets://lights.json", json))
+ return;
+
+ auto &scene = scene_loader.get_scene();
+ rapidjson::Document doc;
+ doc.Parse(json);
+
+ if (doc.HasMember("directional"))
+ {
+ auto &dir = doc["directional"];
+ auto &dir_lights = scene.get_entity_pool().get_component_group();
+ DirectionalLightComponent *comp;
+
+ if (dir_lights.empty())
+ {
+ auto light = scene.create_entity();
+ comp = light->allocate_component();
+ }
+ else
+ comp = get_component(dir_lights.front());
+
+ for (int i = 0; i < 3; i++)
+ {
+ comp->direction[i] = -dir["direction"][i].GetFloat();
+ comp->color[i] = dir["color"][i].GetFloat();
+ }
+ }
+
+ if (doc.HasMember("spot"))
+ {
+ auto &spots = doc["spot"];
+ for (auto itr = spots.Begin(); itr != spots.End(); ++itr)
+ {
+ auto &spot = *itr;
+ SceneFormats::LightInfo info;
+ info.type = SceneFormats::LightInfo::Type::Spot;
+ info.inner_cone = spot["innerCone"].GetFloat();
+ info.outer_cone = spot["outerCone"].GetFloat();
+ for (int i = 0; i < 3; i++)
+ info.color[i] = spot["color"][i].GetFloat();
+ if (spot.HasMember("range"))
+ info.range = spot["range"].GetFloat();
+
+ auto node = scene.create_node();
+ for (int i = 0; i < 3; i++)
+ node->get_transform().translation[i] = spot["position"][i].GetFloat();
+
+ auto &dir = spot["direction"];
+ node->get_transform().rotation = conjugate(look_at_arbitrary_up(vec3(
+ dir[0].GetFloat(), dir[1].GetFloat(), dir[2].GetFloat())));
+ scene.get_root_node()->add_child(node);
+ auto entity = scene.create_light(info, node.get());
+ entity->allocate_component();
+ }
+ }
+
+ if (doc.HasMember("point"))
+ {
+ auto &points = doc["point"];
+ for (auto itr = points.Begin(); itr != points.End(); ++itr)
+ {
+ auto &point = *itr;
+ SceneFormats::LightInfo info;
+ info.type = SceneFormats::LightInfo::Type::Point;
+ for (int i = 0; i < 3; i++)
+ info.color[i] = point["color"][i].GetFloat();
+ if (point.HasMember("range"))
+ info.range = point["range"].GetFloat();
+
+ auto node = scene.create_node();
+ for (int i = 0; i < 3; i++)
+ node->get_transform().translation[i] = point["position"][i].GetFloat();
+ scene.get_root_node()->add_child(node);
+ auto entity = scene.create_light(info, node.get());
+ entity->allocate_component();
+ }
+ }
+}
+
+void SceneViewerApplication::read_quirks(const std::string &path)
+{
+ std::string json;
+ if (!GRANITE_FILESYSTEM()->read_file_to_string(path, json))
+ {
+ LOGE("Failed to read quirks file. Assuming defaults.\n");
+ return;
+ }
+
+ rapidjson::Document doc;
+ doc.Parse(json);
+
+ if (doc.HasMember("instanceDeferredLights"))
+ ImplementationQuirks::get().instance_deferred_lights = doc["instanceDeferredLights"].GetBool();
+ if (doc.HasMember("mergeSubpasses"))
+ ImplementationQuirks::get().merge_subpasses = doc["mergeSubpasses"].GetBool();
+ if (doc.HasMember("useTransientColor"))
+ ImplementationQuirks::get().use_transient_color = doc["useTransientColor"].GetBool();
+ if (doc.HasMember("useTransientDepthStencil"))
+ ImplementationQuirks::get().use_transient_depth_stencil = doc["useTransientDepthStencil"].GetBool();
+ if (doc.HasMember("queueWaitOnSubmission"))
+ ImplementationQuirks::get().queue_wait_on_submission = doc["queueWaitOnSubmission"].GetBool();
+ if (doc.HasMember("useAsyncComputePost"))
+ ImplementationQuirks::get().use_async_compute_post = doc["useAsyncComputePost"].GetBool();
+ if (doc.HasMember("renderGraphForceSingleQueue"))
+ ImplementationQuirks::get().render_graph_force_single_queue = doc["renderGraphForceSingleQueue"].GetBool();
+ if (doc.HasMember("forceNoSubgroups"))
+ ImplementationQuirks::get().force_no_subgroups = doc["forceNoSubgroups"].GetBool();
+ if (doc.HasMember("forceNoSubgroupShuffle"))
+ ImplementationQuirks::get().force_no_subgroup_shuffle = doc["forceNoSubgroupShuffle"].GetBool();
+ if (doc.HasMember("forceNoSubgroupSizeControl"))
+ ImplementationQuirks::get().force_no_subgroup_size_control = doc["forceNoSubgroupSizeControl"].GetBool();
+}
+
+void SceneViewerApplication::read_config(const std::string &path)
+{
+ std::string json;
+ if (!GRANITE_FILESYSTEM()->read_file_to_string(path, json))
+ {
+ LOGE("Failed to read config file. Assuming defaults.\n");
+ return;
+ }
+
+ rapidjson::Document doc;
+ doc.Parse(json);
+
+ if (doc.HasMember("renderer"))
+ {
+ auto *renderer = doc["renderer"].GetString();
+ if (strcmp(renderer, "forward") == 0)
+ config.renderer_type = RendererType::GeneralForward;
+ else if (strcmp(renderer, "deferred") == 0)
+ config.renderer_type = RendererType::GeneralDeferred;
+ else
+ throw std::invalid_argument("Invalid renderer option.");
+ }
+
+ if (doc.HasMember("msaa"))
+ config.msaa = doc["msaa"].GetUint();
+
+ if (doc.HasMember("ssao"))
+ config.ssao = doc["ssao"].GetBool();
+
+ if (doc.HasMember("ssr"))
+ config.ssr = doc["ssr"].GetBool();
+
+ if (doc.HasMember("debugProbes"))
+ config.debug_probes = doc["debugProbes"].GetBool();
+
+ if (doc.HasMember("directionalLightShadows"))
+ config.directional_light_shadows = doc["directionalLightShadows"].GetBool();
+
+ if (doc.HasMember("directionalLightShadowsCascaded"))
+ config.directional_light_cascaded_shadows = doc["directionalLightShadowsCascaded"].GetBool();
+
+ if (doc.HasMember("directionalLightShadowsVSM"))
+ config.directional_light_shadows_vsm = doc["directionalLightShadowsVSM"].GetBool();
+
+ if (doc.HasMember("PCFKernelWide"))
+ {
+ bool wide = doc["PCFKernelWide"].GetBool();
+ if (wide)
+ config.pcf_flags = SCENE_RENDERER_SHADOW_PCF_WIDE_BIT;
+ else
+ config.pcf_flags = 0;
+ renderer_suite_config.pcf_wide = wide;
+ }
+ if (doc.HasMember("clusteredLightsShadows"))
+ config.clustered_lights_shadows = doc["clusteredLightsShadows"].GetBool();
+ if (doc.HasMember("clusteredLightsShadowsResolution"))
+ config.clustered_lights_shadow_resolution = doc["clusteredLightsShadowsResolution"].GetUint();
+ if (doc.HasMember("clusteredLightsShadowsVSM"))
+ config.clustered_lights_shadows_vsm = doc["clusteredLightsShadowsVSM"].GetBool();
+ if (doc.HasMember("hdrBloom"))
+ config.hdr_bloom = doc["hdrBloom"].GetBool();
+ if (doc.HasMember("hdrBloomDynamicExposure"))
+ config.hdr_bloom_dynamic_exposure = doc["hdrBloomDynamicExposure"].GetBool();
+ if (doc.HasMember("showUi"))
+ config.show_ui = doc["showUi"].GetBool();
+ if (doc.HasMember("forwardDepthPrepass"))
+ config.forward_depth_prepass = doc["forwardDepthPrepass"].GetBool();
+
+ if (doc.HasMember("shadowMapResolution"))
+ config.shadow_map_resolution = doc["shadowMapResolution"].GetFloat();
+
+ if (doc.HasMember("renderTargetFp16"))
+ config.rt_fp16 = doc["renderTargetFp16"].GetBool();
+
+ if (doc.HasMember("rescaleScene"))
+ config.rescale_scene = doc["rescaleScene"].GetBool();
+
+ if (doc.HasMember("postAA"))
+ {
+ auto *aa = doc["postAA"].GetString();
+ config.postaa_type = string_to_post_antialiasing_type(aa);
+ }
+
+ if (doc.HasMember("resolutionScale"))
+ config.resolution_scale = doc["resolutionScale"].GetFloat();
+ if (doc.HasMember("resolutionScaleSharpen"))
+ config.resolution_scale_sharpen = doc["resolutionScaleSharpen"].GetBool();
+
+ if (doc.HasMember("lodBias"))
+ config.lod_bias = doc["lodBias"].GetFloat();
+
+ if (doc.HasMember("volumetricFog"))
+ config.volumetric_fog = doc["volumetricFog"].GetBool();
+ if (doc.HasMember("volumetricDiffuse"))
+ config.volumetric_diffuse = doc["volumetricDiffuse"].GetBool();
+}
+
+SceneViewerApplication::SceneViewerApplication(const std::string &path, const std::string &config_path,
+ const std::string &quirks_path, const CLIConfig &cli_config_)
+ : cli_config(cli_config_)
+{
+ GRANITE_ASSET_MANAGER()->enable_mesh_assets();
+
+ renderer_suite.set_default_renderers();
+ if (!renderer_suite.load_variant_cache("assets://renderer_suite_variants.json"))
+ renderer_suite.load_variant_cache("cache://renderer_suite_variants.json");
+
+ if (!config_path.empty())
+ read_config(config_path);
+ if (!quirks_path.empty())
+ read_quirks(quirks_path);
+ renderer_suite_config.cascaded_directional_shadows = config.directional_light_cascaded_shadows;
+ renderer_suite_config.directional_light_vsm = config.directional_light_shadows_vsm;
+
+ scene_loader.load_scene(path);
+ read_lights();
+
+ scene_transform_manager.init(scene_loader.get_scene());
+
+ if (cli_config.ocean)
+ {
+ OceanConfig ocean_config = {};
+ //ocean_config.grid_count = 4;
+ //ocean_config.ocean_size = vec2(32.0f, 32.0f);
+ //ocean_config.fft_resolution = ocean_config.grid_count * ocean_config.grid_resolution;
+ //ocean_config.heightmap = true;
+ //ocean_config.lod_bias = -2.0f;
+ //auto &scene = scene_loader.get_scene();
+ //auto node = scene.create_node();
+ //node->transform.translation = vec3(30.0f, -5.0f, 40.0f);
+ //node->invalidate_cached_transform();
+ //Ocean::add_to_scene(scene_loader.get_scene(), ocean_config, node);
+ Ocean::add_to_scene(scene_loader.get_scene(), ocean_config);
+ //scene.get_root_node()->add_child(std::move(node));
+ }
+
+ if (false && config.volumetric_diffuse)
+ {
+ auto &scene = scene_loader.get_scene();
+ auto node = scene.create_node();
+ node->get_transform().scale = vec3(32.0f, 8.0f, 32.0f);
+ node->get_transform().translation = vec3(0.0f, 3.5f, 0.0f);
+ node->invalidate_cached_transform();
+ scene.create_volumetric_diffuse_light(uvec3(32, 8, 32), node.get());
+ scene.get_root_node()->add_child(std::move(node));
+ }
+
+ if (config.volumetric_fog_regions && config.volumetric_fog)
+ {
+ auto &scene = scene_loader.get_scene();
+ auto node = scene.create_node();
+ node->get_transform().scale = vec3(40.0f);
+ node->get_transform().translation = vec3(0.0f, 20.0f, 0.0f);
+ node->invalidate_cached_transform();
+ scene.create_volumetric_fog_region(node.get());
+ scene.get_root_node()->add_child(std::move(node));
+ }
+
+#if 0
+ for (int z = -8; z <= 8; z++)
+ {
+ for (int x = -8; x <= 8; x++)
+ {
+ auto &scene = scene_loader.get_scene();
+ auto node = scene.create_node();
+ node->transform.scale = vec3(1.0f);
+ node->transform.translation = vec3(2.0f * x, 0.4f, 2.0f * z);
+ node->transform.rotation = angleAxis(half_pi(), vec3(1.0f, 0.0f, 0.0f));
+ node->invalidate_cached_transform();
+ scene.create_volumetric_decal(node.get());
+ scene.get_root_node()->add_child(std::move(node));
+ }
+ }
+#endif
+
+ animation_system = scene_loader.consume_animation_system();
+ context.set_lighting_parameters(&lighting);
+ scene_transform_manager.register_persistent_render_context(&context);
+ fallback_depth_context.set_lighting_parameters(&fallback_lighting);
+
+ scene_transform_manager.register_persistent_render_context(&fallback_depth_context);
+ for (auto &d : depth_contexts)
+ scene_transform_manager.register_persistent_render_context(&d);
+ cam.set_depth_range_infinite(1.0f / 16.0f);
+
+ // Create a dummy background if there isn't any background.
+ if (scene_loader.get_scene().get_entity_pool().get_component_group().empty())
+ {
+ auto dome = Util::make_handle();
+ scene_loader.get_scene().create_renderable(dome, nullptr);
+ }
+
+ auto *environment = scene_loader.get_scene().get_environment();
+ if (environment)
+ lighting.fog = environment->fog;
+ else
+ lighting.fog = {};
+
+ cam.look_at(vec3(0.0f, 0.0f, 8.0f), vec3(0.0f));
+
+ // Pick a camera to show.
+ selected_camera = &cam;
+
+ if (cli_config.camera_index >= 0)
+ {
+ auto &scene_cameras = scene_loader.get_scene().get_entity_pool().get_component_group();
+ if (!scene_cameras.empty())
+ {
+ if (unsigned(cli_config.camera_index) < scene_cameras.size())
+ selected_camera = &get_component(scene_cameras[cli_config.camera_index])->camera;
+ else
+ LOGE("Camera index is out of bounds, using normal camera.");
+ }
+ }
+
+ // Pick a directional light.
+ default_directional_light.color = vec3(6.0f, 5.5f, 4.5f);
+ default_directional_light.direction = light_direction();
+ auto &dir_lights = scene_loader.get_scene().get_entity_pool().get_component_group();
+ if (!dir_lights.empty())
+ selected_directional = get_component(dir_lights.front());
+ else
+ selected_directional = &default_directional_light;
+
+ {
+ cluster = std::make_unique();
+ auto entity = scene_loader.get_scene().create_entity();
+ auto *refresh = entity->allocate_component();
+ refresh->refresh = cluster.get();
+
+ auto *rp = entity->allocate_component();
+ rp->creator = cluster.get();
+ lighting.cluster = cluster.get();
+
+ cluster->set_enable_shadows(config.clustered_lights_shadows);
+ cluster->set_shadow_resolution(config.clustered_lights_shadow_resolution);
+ cluster->set_enable_volumetric_fog(config.volumetric_fog && config.volumetric_fog_regions);
+
+ if (config.clustered_lights_shadows_vsm)
+ cluster->set_shadow_type(LightClusterer::ShadowType::VSM);
+ else
+ cluster->set_shadow_type(LightClusterer::ShadowType::PCF);
+
+ cluster->set_resolution(128, 64, 4 * 1024);
+ }
+
+ if (config.volumetric_fog)
+ {
+ volumetric_fog = std::make_unique();
+ volumetric_fog->set_resolution(160, 92, 128);
+ volumetric_fog->set_z_range(80.0f);
+ lighting.volumetric_fog = volumetric_fog.get();
+ auto entity = scene_loader.get_scene().create_entity();
+ auto *rp = entity->allocate_component();
+ rp->creator = volumetric_fog.get();
+
+ volumetric_fog->add_storage_buffer_dependency("cluster-bitmask");
+ volumetric_fog->add_storage_buffer_dependency("cluster-range");
+ volumetric_fog->add_storage_buffer_dependency("cluster-transforms");
+
+ if (config.directional_light_shadows)
+ volumetric_fog->add_texture_dependency("shadow-main");
+ }
+
+ if (config.volumetric_diffuse)
+ {
+ volumetric_diffuse = std::make_unique();
+ lighting.volumetric_diffuse = volumetric_diffuse.get();
+
+ auto entity = scene_loader.get_scene().create_entity();
+
+ auto *rp = entity->allocate_component();
+ rp->creator = volumetric_diffuse.get();
+
+ auto *update = entity->allocate_component();
+ // Must come before clusterer since we're modifying volumetric light textures,
+ // which will affect clustering, since it writes new descriptors.
+ update->dependency_order = -1;
+ update->refresh = volumetric_diffuse.get();
+
+ volumetric_diffuse->set_fallback_render_context(&fallback_depth_context);
+ }
+
+ if (cluster)
+ {
+ cluster->set_enable_volumetric_diffuse(config.volumetric_diffuse);
+ cluster->set_enable_volumetric_decals(false);
+ }
+
+ context.set_camera(*selected_camera);
+
+ graph.enable_timestamps(cli_config.timestamp);
+
+ if (config.rescale_scene)
+ rescale_scene(10.0f);
+
+ EVENT_MANAGER_REGISTER_LATCH(SceneViewerApplication, on_swapchain_changed, on_swapchain_destroyed,
+ SwapchainParameterEvent);
+ EVENT_MANAGER_REGISTER_LATCH(SceneViewerApplication, on_device_created, on_device_destroyed, DeviceCreatedEvent);
+ EVENT_MANAGER_REGISTER(SceneViewerApplication, on_key_down, KeyboardEvent);
+}
+
+void SceneViewerApplication::export_lights()
+{
+ auto lights = export_lights_to_json(lighting.directional, scene_loader.get_scene());
+ if (!GRANITE_FILESYSTEM()->write_string_to_file("cache://lights.json", lights))
+ LOGE("Failed to export light data.\n");
+}
+
+void SceneViewerApplication::export_cameras()
+{
+ auto cameras = export_cameras_to_json(recorded_cameras);
+ if (!GRANITE_FILESYSTEM()->write_string_to_file("cache://cameras.json", cameras))
+ LOGE("Failed to export camera data.\n");
+}
+
+SceneViewerApplication::~SceneViewerApplication()
+{
+ export_lights();
+ export_cameras();
+ renderer_suite.save_variant_cache("cache://renderer_suite_variants.json");
+}
+
+void SceneViewerApplication::loop_animations()
+{
+}
+
+void SceneViewerApplication::rescale_scene(float radius)
+{
+ scene_loader.get_scene().update_all_transforms();
+
+ AABB aabb(vec3(FLT_MAX), vec3(-FLT_MAX));
+ auto &objects = scene_loader.get_scene()
+ .get_entity_pool()
+ .get_component_group();
+ for (auto &caster : objects)
+ aabb.expand(get_component(caster)->get_aabb());
+
+ float scale_factor = radius / aabb.get_radius();
+ auto root_node = scene_loader.get_scene().get_root_node();
+ auto new_root_node = scene_loader.get_scene().create_node();
+ new_root_node->get_transform().scale = vec3(scale_factor);
+ new_root_node->add_child(root_node);
+ scene_loader.get_scene().set_root_node(new_root_node);
+}
+
+void SceneViewerApplication::on_device_created(const DeviceCreatedEvent &device)
+{
+ graph.set_device(&device.get_device());
+ context.set_device(&device.get_device());
+ fallback_depth_context.set_device(&device.get_device());
+}
+
+void SceneViewerApplication::on_device_destroyed(const DeviceCreatedEvent &)
+{
+ graph.set_device(nullptr);
+}
+
+bool SceneViewerApplication::on_key_down(const KeyboardEvent &e)
+{
+ if (e.get_key_state() != KeyState::Pressed)
+ return true;
+
+ switch (e.get_key())
+ {
+ case Key::O:
+ selected_camera->set_ortho(!selected_camera->get_ortho(), 5.0f);
+ break;
+
+ case Key::X:
+ {
+ vec3 pos = selected_camera->get_position();
+ auto &scene = scene_loader.get_scene();
+ auto node = scene.create_node();
+ scene.get_root_node()->add_child(node);
+
+ SceneFormats::LightInfo light;
+ light.type = SceneFormats::LightInfo::Type::Spot;
+ light.outer_cone = 0.9f;
+ light.inner_cone = 0.92f;
+ light.color = vec3(10.0f);
+
+ node->get_transform().translation = pos;
+ node->get_transform().rotation = conjugate(look_at_arbitrary_up(selected_camera->get_front()));
+
+ auto *entity = scene.create_light(light, node.get());
+ entity->allocate_component();
+ break;
+ }
+
+ case Key::C:
+ {
+ vec3 pos = selected_camera->get_position();
+ auto &scene = scene_loader.get_scene();
+ auto node = scene.create_node();
+ scene.get_root_node()->add_child(node);
+
+ SceneFormats::LightInfo light;
+ light.type = SceneFormats::LightInfo::Type::Point;
+ light.color = vec3(10.0f);
+ node->get_transform().translation = pos;
+
+ auto *entity = scene.create_light(light, node.get());
+ entity->allocate_component();
+ break;
+ }
+
+ case Key::V:
+ {
+ default_directional_light.direction = -selected_camera->get_front();
+ selected_directional = &default_directional_light;
+ need_shadow_map_update = true;
+ break;
+ }
+
+ case Key::B:
+ {
+ float fovy = selected_camera->get_fovy();
+ float aspect = selected_camera->get_aspect();
+ float znear = selected_camera->get_znear();
+ float zfar = selected_camera->get_zfar();
+
+ RecordedCamera camera;
+ camera.direction = selected_camera->get_front();
+ camera.position = selected_camera->get_position();
+ camera.up = selected_camera->get_up();
+ camera.aspect = aspect;
+ camera.fovy = fovy;
+ camera.znear = znear;
+ camera.zfar = zfar;
+ recorded_cameras.push_back(camera);
+ break;
+ }
+
+ case Key::R:
+ {
+ auto &scene = scene_loader.get_scene();
+ scene.remove_entities_with_component();
+ break;
+ }
+
+ case Key::K:
+ {
+ capture_environment_probe();
+ break;
+ }
+
+ case Key::Space:
+ {
+ auto mode = get_wsi().get_present_mode();
+ if (mode == PresentMode::SyncToVBlank)
+ get_wsi().set_present_mode(PresentMode::UnlockedMaybeTear);
+ else
+ get_wsi().set_present_mode(PresentMode::SyncToVBlank);
+ break;
+ }
+
+ case Key::M:
+ {
+ get_wsi().set_backbuffer_srgb(!get_wsi().get_backbuffer_srgb());
+ break;
+ }
+
+ case Key::H:
+ {
+ get_wsi().set_backbuffer_format(get_wsi().get_backbuffer_format() == Vulkan::BackbufferFormat::HDR10 ?
+ Vulkan::BackbufferFormat::sRGB : Vulkan::BackbufferFormat::HDR10);
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void SceneViewerApplication::capture_environment_probe()
+{
+ ImageCreateInfo info = ImageCreateInfo::render_target(512, 512, VK_FORMAT_R16G16B16A16_SFLOAT);
+ info.levels = 1;
+ info.layers = 6;
+ info.usage |= VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
+ info.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
+ info.initial_layout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL;
+ auto &device = get_wsi().get_device();
+
+ auto handle = device.create_image(info, nullptr);
+ auto cmd = device.request_command_buffer();
+
+ VisibilityList visible;
+
+ for (unsigned face = 0; face < 6; face++)
+ {
+ ImageViewCreateInfo view_info = {};
+ view_info.layers = 1;
+ view_info.base_layer = face;
+ view_info.format = info.format;
+ view_info.levels = 1;
+ view_info.image = handle.get();
+ auto rt_view = device.create_image_view(view_info);
+
+ mat4 proj, view;
+ compute_cube_render_transform(selected_camera->get_position(), face, proj, view, 0.1f, 300.0f);
+ context.set_camera(proj, view);
+
+ RenderPassInfo rp = {};
+ rp.num_color_attachments = 1;
+ rp.color_attachments[0] = rt_view.get();
+ rp.store_attachments = 1;
+ rp.clear_attachments = 1;
+
+ auto depth_att = device.get_transient_attachment(512, 512, device.get_default_depth_format(), 0);
+ rp.depth_stencil = &depth_att->get_view();
+ rp.op_flags = RENDER_PASS_OP_CLEAR_DEPTH_STENCIL_BIT;
+ rp.clear_depth_stencil.depth = 0.0f;
+ rp.clear_depth_stencil.stencil = 0;
+ rp.clear_color[0].float32[0] = 0.0f;
+ rp.clear_color[0].float32[1] = 0.0f;
+ rp.clear_color[0].float32[2] = 0.0f;
+ rp.clear_color[0].float32[3] = 1.0f;
+ cmd->begin_render_pass(rp);
+
+ auto &scene = scene_loader.get_scene();
+ visible.clear();
+ scene.gather_visible_opaque_renderables(context.get_visibility_frustum(), visible);
+ scene.gather_visible_render_pass_sinks(context.get_render_parameters().camera_position, visible);
+ scene.gather_unbounded_renderables(visible);
+
+ auto &forward_renderer = renderer_suite.get_renderer(RendererSuite::Type::ForwardOpaque);
+ forward_renderer.set_mesh_renderer_options_from_lighting(lighting);
+ forward_renderer.set_mesh_renderer_options(forward_renderer.get_mesh_renderer_options() | config.pcf_flags);
+
+ forward_renderer.begin(queue);
+ queue.push_renderables(context, visible.data(), visible.size());
+
+ Renderer::RendererOptionFlags opt = Renderer::FRONT_FACE_CLOCKWISE_BIT;
+ forward_renderer.flush(*cmd, queue, context, opt);
+
+ cmd->end_render_pass();
+ }
+
+ cmd->image_barrier(*handle, VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+ VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_TRANSFER_READ_BIT);
+ device.submit(cmd);
+ auto buffer = save_image_to_cpu_buffer(device, *handle, CommandBuffer::Type::Generic);
+ save_image_buffer_to_gtx(device, buffer, "cache://environment.gtx");
+}
+
+static inline std::string tagcat(const std::string &a, const std::string &b)
+{
+ return a + "-" + b;
+}
+
+void SceneViewerApplication::add_mv_pass(const std::string &tag, const std::string &depth, bool full_mv)
+{
+ AttachmentInfo mv;
+ mv.size_class = SizeClass::InputRelative;
+ mv.size_relative_name = depth;
+ mv.format = VK_FORMAT_R16G16_SFLOAT;
+
+ auto &mv_pass = graph.add_pass(tagcat("mv", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ mv_pass.set_depth_stencil_input(depth);
+ mv_pass.add_color_output(tagcat("mv", tag), mv);
+ culling_passes_info.motion_vector_pass = &mv_pass;
+
+ if (full_mv)
+ mv_pass.add_attachment_input(depth);
+
+ auto renderer = Util::make_handle();
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.context = &context;
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_MOTION_VECTOR_BIT;
+ if (full_mv)
+ setup.flags |= SCENE_RENDERER_MOTION_VECTOR_FULL_BIT;
+
+ // After normal rendering, the occlusion state buffer is exactly what we want.
+ // Main downside is that we have to split render pass if we're doing forward rendering with task,
+ // but that's fairly minor.
+ renderer->set_extra_flush_flags(Renderer::MESH_ASSET_PHASE_1_BIT);
+
+ renderer->init(setup);
+
+ mv_pass.set_render_pass_interface(std::move(renderer));
+}
+
+void SceneViewerApplication::add_main_pass_forward(Device &device, const std::string &tag)
+{
+ AttachmentInfo color, depth;
+ depth.format = device.get_default_depth_format();
+
+ color.size_x = config.resolution_scale;
+ color.size_y = config.resolution_scale;
+ depth.size_x = config.resolution_scale;
+ depth.size_y = config.resolution_scale;
+
+ //bool use_ssao = config.forward_depth_prepass && config.ssao && config.msaa == 1;
+ const bool use_ssao = false;
+
+ auto &phase1 = graph.add_pass(tagcat("depth-phase1", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ phase1.set_depth_stencil_output(tagcat("depth-prepass", tag), depth);
+ culling_passes_info.phase1_depth_output = tagcat("depth-prepass", tag);
+
+ scene_loader.get_scene().add_render_pass_dependencies(graph, phase1,
+ RenderPassCreator::GEOMETRY_BIT |
+ RenderPassCreator::MATERIAL_BIT);
+
+ {
+ auto renderer = Util::make_handle();
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.context = &context;
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_Z_PREPASS_BIT;
+ if (config.debug_probes)
+ setup.flags |= SCENE_RENDERER_DEBUG_PROBES_BIT;
+ renderer->set_extra_flush_flags(Renderer::MESH_ASSET_PHASE_1_BIT);
+ renderer->init(setup);
+ phase1.set_render_pass_interface(std::move(renderer));
+ }
+
+ if (use_ssao)
+ {
+ auto &prepass_depth = graph.add_pass(tagcat("depth-transient", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ prepass_depth.set_depth_stencil_output(tagcat("depth-transient", tag), depth);
+ auto renderer = Util::make_handle();
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.context = &context;
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_Z_PREPASS_BIT;
+ renderer->init(setup);
+
+ // TODO: Find a good way to let the prepass renderer share renderer with opaque / transparent passes.
+ prepass_depth.set_render_pass_interface(std::move(renderer));
+ scene_loader.get_scene().add_render_pass_dependencies(graph, prepass_depth, RenderPassCreator::GEOMETRY_BIT);
+ setup_ffx_cacao(graph, context, tagcat("ssao-output", tag), tagcat("depth-transient", tag), "");
+ }
+
+ bool supports_32bpp =
+ device.image_format_is_supported(VK_FORMAT_B10G11R11_UFLOAT_PACK32,
+ VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT);
+
+ if (config.hdr_bloom || get_wsi().get_backbuffer_color_space() == VK_COLOR_SPACE_HDR10_ST2084_EXT)
+ color.format =
+ (config.rt_fp16 || !supports_32bpp) ? VK_FORMAT_R16G16B16A16_SFLOAT : VK_FORMAT_B10G11R11_UFLOAT_PACK32;
+ else
+ color.format = VK_FORMAT_UNDEFINED; // Swapchain format.
+
+ color.samples = config.msaa;
+ depth.samples = config.msaa;
+
+ auto resolved = color;
+ resolved.samples = 1;
+
+ auto &lighting_pass = graph.add_pass(tagcat("lighting", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+
+ culling_passes_info.phase1_pass = &phase1;
+ culling_passes_info.phase2_pass = &lighting_pass;
+ culling_passes_info.force_visible_phase2 = true;
+
+ if (color.samples > 1)
+ {
+ lighting_pass.add_color_output(tagcat("HDR-MS", tag), color);
+ lighting_pass.add_resolve_output(tagcat("HDR", tag), resolved);
+ }
+ else
+ lighting_pass.add_color_output(tagcat("HDR", tag), color);
+
+ if (use_ssao)
+ {
+ ssao_output = &lighting_pass.add_texture_input(tagcat("ssao-output", tag));
+ lighting_pass.set_depth_stencil_input(tagcat("depth-transient", tag));
+ lighting_pass.add_fake_resource_write_alias(tagcat("depth-transient", tag), tagcat("depth", tag));
+ }
+ else
+ {
+ ssao_output = nullptr;
+ lighting_pass.set_depth_stencil_output(tagcat("depth", tag), depth);
+ }
+
+ auto renderer = Util::make_handle();
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.context = &context;
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_FORWARD_OPAQUE_BIT | SCENE_RENDERER_FORWARD_TRANSPARENT_BIT | config.pcf_flags;
+ if (config.forward_depth_prepass && !use_ssao)
+ setup.flags |= SCENE_RENDERER_Z_PREPASS_BIT;
+ else if (config.forward_depth_prepass)
+ setup.flags |= SCENE_RENDERER_Z_EXISTING_PREPASS_BIT;
+
+ if (config.debug_probes)
+ setup.flags |= SCENE_RENDERER_DEBUG_PROBES_BIT;
+
+ renderer->set_extra_flush_flags(Renderer::MESH_ASSET_PHASE_2_BIT | Renderer::MESH_ASSET_FORCE_ALL_VISIBLE_BIT);
+ renderer->init(setup);
+
+ lighting_pass.set_render_pass_interface(std::move(renderer));
+
+ shadows = nullptr;
+ if (config.directional_light_shadows)
+ shadows = &lighting_pass.add_texture_input("shadow-main");
+ scene_loader.get_scene().add_render_pass_dependencies(graph, lighting_pass,
+ RenderPassCreator::LIGHTING_BIT |
+ RenderPassCreator::GEOMETRY_BIT |
+ RenderPassCreator::MATERIAL_BIT);
+}
+
+void SceneViewerApplication::add_main_pass_deferred(Device &device, const std::string &tag)
+{
+ bool supports_32bpp =
+ device.image_format_is_supported(VK_FORMAT_B10G11R11_UFLOAT_PACK32, VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT);
+ AttachmentInfo emissive, albedo, normal, pbr, depth;
+ if (config.hdr_bloom || get_wsi().get_backbuffer_color_space() == VK_COLOR_SPACE_HDR10_ST2084_EXT)
+ emissive.format =
+ (config.rt_fp16 || !supports_32bpp) ? VK_FORMAT_R16G16B16A16_SFLOAT : VK_FORMAT_B10G11R11_UFLOAT_PACK32;
+ else
+ emissive.format = VK_FORMAT_UNDEFINED;
+
+ const auto set_scale = [&](AttachmentInfo &info) {
+ info.size_x = config.resolution_scale;
+ info.size_y = config.resolution_scale;
+ };
+ set_scale(emissive);
+ set_scale(albedo);
+ set_scale(normal);
+ set_scale(pbr);
+ set_scale(depth);
+
+ albedo.format = VK_FORMAT_R8G8B8A8_SRGB;
+ normal.format = VK_FORMAT_A2B10G10R10_UNORM_PACK32;
+ pbr.format = VK_FORMAT_R8G8_UNORM;
+ depth.format = device.get_default_depth_format();
+
+ culling_passes_info.phase1_depth_output = tagcat("depth-prepass", tag);
+
+ auto &phase1 = graph.add_pass(tagcat("gbuffer-phase1", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ phase1.set_depth_stencil_output(culling_passes_info.phase1_depth_output, depth);
+
+ scene_loader.get_scene().add_render_pass_dependencies(graph, phase1,
+ RenderPassCreator::GEOMETRY_BIT |
+ RenderPassCreator::MATERIAL_BIT);
+
+ {
+ auto renderer = Util::make_handle();
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.context = &context;
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_Z_PREPASS_BIT;
+ if (config.debug_probes)
+ setup.flags |= SCENE_RENDERER_DEBUG_PROBES_BIT;
+ renderer->set_extra_flush_flags(Renderer::MESH_ASSET_PHASE_1_BIT);
+ renderer->init(setup);
+ phase1.set_render_pass_interface(std::move(renderer));
+ }
+
+ auto &gbuffer = graph.add_pass(tagcat("gbuffer", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ gbuffer.add_color_output(tagcat("emissive", tag), emissive);
+ gbuffer.add_color_output(tagcat("albedo", tag), albedo);
+ gbuffer.add_color_output(tagcat("normal", tag), normal);
+ gbuffer.add_color_output(tagcat("pbr", tag), pbr);
+ gbuffer.set_depth_stencil_output(tagcat("depth-transient", tag), depth);
+ gbuffer.set_depth_stencil_input(tagcat("depth-prepass", tag));
+
+ culling_passes_info.phase1_pass = &phase1;
+ culling_passes_info.phase2_pass = &gbuffer;
+
+ {
+ auto renderer = Util::make_handle();
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.context = &context;
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_DEFERRED_GBUFFER_BIT;
+ if (config.debug_probes)
+ setup.flags |= SCENE_RENDERER_DEBUG_PROBES_BIT;
+ renderer->set_extra_flush_flags(Renderer::MESH_ASSET_PHASE_2_BIT | Renderer::MESH_ASSET_FORCE_ALL_VISIBLE_BIT);
+ renderer->init(setup);
+ gbuffer.set_render_pass_interface(std::move(renderer));
+ }
+
+ if (config.ssao)
+ {
+ setup_ffx_cacao(graph, context, tagcat("ssao-output", tag), tagcat("depth-transient", tag),
+ tagcat("normal", tag));
+ }
+
+ auto &lighting_pass = graph.add_pass(tagcat("lighting", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ lighting_pass.add_color_output(tagcat("HDR", tag), emissive, tagcat("emissive", tag));
+ lighting_pass.add_attachment_input(tagcat("albedo", tag));
+ lighting_pass.add_attachment_input(tagcat("normal", tag));
+ lighting_pass.add_attachment_input(tagcat("pbr", tag));
+ lighting_pass.add_attachment_input(tagcat("depth-transient", tag));
+ lighting_pass.set_depth_stencil_input(tagcat("depth-transient", tag));
+ lighting_pass.add_fake_resource_write_alias(tagcat("depth-transient", tag), tagcat("depth", tag));
+
+ {
+ auto renderer = Util::make_handle();
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.context = &context;
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_DEFERRED_LIGHTING_BIT | SCENE_RENDERER_FORWARD_TRANSPARENT_BIT | config.pcf_flags;
+ renderer->init(setup);
+
+ lighting_pass.set_render_pass_interface(std::move(renderer));
+ }
+
+ if (config.ssao)
+ ssao_output = &lighting_pass.add_texture_input(tagcat("ssao-output", tag));
+ else
+ ssao_output = nullptr;
+
+ shadows = nullptr;
+ if (config.directional_light_shadows)
+ shadows = &lighting_pass.add_texture_input("shadow-main");
+
+ scene_loader.get_scene().add_render_pass_dependencies(graph, gbuffer,
+ RenderPassCreator::GEOMETRY_BIT |
+ RenderPassCreator::MATERIAL_BIT);
+ scene_loader.get_scene().add_render_pass_dependencies(graph, lighting_pass,
+ RenderPassCreator::LIGHTING_BIT);
+}
+
+void SceneViewerApplication::add_main_pass(Device &device, const std::string &tag)
+{
+ switch (config.renderer_type)
+ {
+ case RendererType::GeneralForward:
+ add_main_pass_forward(device, tag);
+ break;
+
+ case RendererType::GeneralDeferred:
+ add_main_pass_deferred(device, tag);
+ break;
+
+ default:
+ break;
+ }
+}
+
+void SceneViewerApplication::add_shadow_pass_fallback(Vulkan::Device &, const std::string &tag)
+{
+ AttachmentInfo shadowmap;
+ shadowmap.format = VK_FORMAT_D16_UNORM;
+ shadowmap.size_class = SizeClass::Absolute;
+ shadowmap.size_x = 2048;
+ shadowmap.size_y = 2048;
+
+ auto &shadowpass = graph.add_pass(tagcat("shadow", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ fallback_shadows = &shadowpass.set_depth_stencil_output(tagcat("shadow", tag), shadowmap);
+
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_DEPTH_BIT | SCENE_RENDERER_DEPTH_DYNAMIC_BIT | SCENE_RENDERER_FALLBACK_DEPTH_BIT;
+ setup.context = &fallback_depth_context;
+
+ auto handle = Util::make_handle();
+ handle->init(setup);
+ shadowpass.set_render_pass_interface(std::move(handle));
+
+ scene_loader.get_scene().add_render_pass_dependencies(graph, shadowpass,
+ RenderPassCreator::GEOMETRY_BIT |
+ RenderPassCreator::MATERIAL_BIT);
+}
+
+void SceneViewerApplication::add_shadow_pass(Device &, const std::string &tag)
+{
+ AttachmentInfo shadowmap;
+ shadowmap.format = VK_FORMAT_D16_UNORM;
+ shadowmap.samples = config.directional_light_shadows_vsm ? 4 : 1;
+ shadowmap.size_class = SizeClass::Absolute;
+ shadowmap.size_x = config.shadow_map_resolution;
+ shadowmap.size_y = config.shadow_map_resolution;
+
+ if (config.directional_light_cascaded_shadows)
+ shadowmap.layers = NumShadowCascades;
+
+ CullingPassesInfo culling = {};
+ culling.phase1_depth_output = tagcat("shadow-prepass", tag);
+ culling.tag = "depth";
+
+ auto &phase1 = graph.add_pass(tagcat("shadow-phase1", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ phase1.set_depth_stencil_output(culling.phase1_depth_output, shadowmap);
+
+ {
+ Util::IntrusivePtr handle;
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_DEPTH_BIT;
+
+ setup.context = depth_contexts;
+ setup.flags |= SCENE_RENDERER_DEPTH_DYNAMIC_BIT;
+ setup.flags |= SCENE_RENDERER_SEPARATE_PER_LAYER_BIT;
+ setup.layers = NumShadowCascades;
+
+ handle = Util::make_handle();
+ handle->set_extra_flush_flags(Renderer::MESH_ASSET_PHASE_1_BIT);
+ handle->init(setup);
+ phase1.set_render_pass_interface(std::move(handle));
+ }
+
+ scene_loader.get_scene().add_render_pass_dependencies(graph, phase1,
+ RenderPassCreator::GEOMETRY_BIT |
+ RenderPassCreator::MATERIAL_BIT);
+
+ auto &shadowpass = graph.add_pass(tagcat("shadow", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+
+ culling.phase1_pass = &phase1;
+ culling.phase2_pass = &shadowpass;
+ culling.contexts = depth_contexts;
+ culling.num_contexts = NumShadowCascades;
+ hiz_depth = &setup_culling_passes(graph, culling);
+
+ if (config.directional_light_shadows_vsm)
+ {
+ auto shadowmap_vsm_color = shadowmap;
+ auto shadowmap_vsm_resolved_color = shadowmap;
+ shadowmap_vsm_color.format = VK_FORMAT_R32G32_SFLOAT;
+ shadowmap_vsm_color.samples = 4;
+ shadowmap_vsm_resolved_color.format = VK_FORMAT_R32G32_SFLOAT;
+ shadowmap_vsm_resolved_color.samples = 1;
+
+ auto shadowmap_vsm_half = shadowmap_vsm_resolved_color;
+ shadowmap_vsm_half.size_x *= 0.5f;
+ shadowmap_vsm_half.size_y *= 0.5f;
+
+ shadowpass.set_depth_stencil_output(tagcat("shadow-depth", tag), shadowmap);
+ shadowpass.set_depth_stencil_input(tagcat("shadow-prepass", tag));
+ shadowpass.add_color_output(tagcat("shadow-msaa", tag), shadowmap_vsm_color);
+ shadowpass.add_resolve_output(tagcat("shadow-raw", tag), shadowmap_vsm_resolved_color);
+
+ auto &down_pass = graph.add_pass(tagcat("shadow-down", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ down_pass.add_color_output(tagcat("shadow-down", tag), shadowmap_vsm_half);
+ auto &down_pass_res = down_pass.add_texture_input(tagcat("shadow-raw", tag));
+
+ auto &up_pass = graph.add_pass(tagcat("shadow-up", tag), RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ up_pass.add_color_output(tagcat("shadow", tag), shadowmap_vsm_resolved_color);
+ auto &up_pass_res = up_pass.add_texture_input(tagcat("shadow-down", tag));
+
+ down_pass.set_build_render_pass([&, layered = shadowmap.layers > 1](CommandBuffer &cmd) {
+ auto &input = graph.get_physical_texture_resource(down_pass_res);
+ vec2 inv_size(1.0f / input.get_image().get_create_info().width,
+ 1.0f / input.get_image().get_create_info().height);
+ cmd.push_constants(&inv_size, 0, sizeof(inv_size));
+ cmd.set_texture(0, 0, input, StockSampler::LinearClamp);
+ CommandBufferUtil::draw_fullscreen_quad(cmd, "builtin://shaders/quad.vert",
+ "builtin://shaders/post/vsm_down_blur.frag",
+ {{ "LAYERED", layered ? 1 : 0 }});
+ });
+
+ up_pass.set_build_render_pass([&, layered = shadowmap.layers > 1](CommandBuffer &cmd) {
+ auto &input = graph.get_physical_texture_resource(up_pass_res);
+ vec2 inv_size(1.0f / input.get_image().get_create_info().width,
+ 1.0f / input.get_image().get_create_info().height);
+ cmd.set_texture(0, 0, input, StockSampler::LinearClamp);
+ cmd.push_constants(&inv_size, 0, sizeof(inv_size));
+ CommandBufferUtil::draw_fullscreen_quad(cmd, "builtin://shaders/quad.vert",
+ "builtin://shaders/post/vsm_up_blur.frag",
+ {{ "LAYERED", layered ? 1 : 0 }});
+ });
+ }
+ else
+ {
+ shadowpass.set_depth_stencil_output(tagcat("shadow", tag), shadowmap);
+ shadowpass.set_depth_stencil_input(tagcat("shadow-prepass", tag));
+ }
+
+ Util::IntrusivePtr handle;
+ RenderPassSceneRenderer::Setup setup = {};
+ setup.scene = &scene_loader.get_scene();
+ setup.suite = &renderer_suite;
+ setup.flags = SCENE_RENDERER_DEPTH_BIT;
+ if (config.directional_light_shadows_vsm)
+ setup.flags |= SCENE_RENDERER_SHADOW_VSM_BIT;
+
+ setup.context = depth_contexts;
+ setup.flags |= SCENE_RENDERER_DEPTH_DYNAMIC_BIT;
+ setup.flags |= SCENE_RENDERER_SEPARATE_PER_LAYER_BIT;
+ setup.layers = NumShadowCascades;
+
+ handle = Util::make_handle();
+ handle->set_extra_flush_flags(Renderer::MESH_ASSET_PHASE_2_BIT);
+ handle->init(setup);
+
+ VkClearColorValue value = {};
+ value.float32[0] = 1.0f;
+ value.float32[1] = 1.0f;
+ handle->set_clear_color(value);
+ shadowpass.set_render_pass_interface(std::move(handle));
+
+ scene_loader.get_scene().add_render_pass_dependencies(graph, shadowpass,
+ RenderPassCreator::GEOMETRY_BIT |
+ RenderPassCreator::MATERIAL_BIT);
+}
+
+void SceneViewerApplication::bake_render_graph(const SwapchainParameterEvent &swap)
+{
+ auto physical_buffers = graph.consume_physical_buffers();
+
+ shadows = nullptr;
+ ssao_output = nullptr;
+ const bool hdr10 = swap.get_color_space() == VK_COLOR_SPACE_HDR10_ST2084_EXT;
+
+ graph.reset();
+
+ // Reclaims memory released by graph.reset(), before we bake and start allocating more.
+ //swap.get_device().wait_idle();
+ swap.get_device().next_frame_context();
+
+ graph.set_device(&swap.get_device());
+
+ swap.get_device().configure_default_geometry_samplers(8.0f, config.lod_bias);
+
+ ResourceDimensions dim;
+ dim.width = swap.get_width();
+ dim.height = swap.get_height();
+ dim.format = swap.get_format();
+ dim.transform = swap.get_prerotate();
+ graph.set_backbuffer_dimensions(dim);
+
+ const char *ui_source = "HDR-main";
+
+ scene_loader.get_scene().add_render_passes(graph);
+
+ if (config.directional_light_shadows)
+ {
+ add_shadow_pass(swap.get_device(), "main");
+ add_shadow_pass_fallback(swap.get_device(), "fallback");
+ }
+
+ add_main_pass(swap.get_device(), "main");
+
+ const char *light_output = "HDR-main";
+
+ if (config.renderer_type == RendererType::GeneralDeferred && config.ssr)
+ {
+ setup_ssr_pass(graph, context, "depth-transient-main",
+ "albedo-main", "normal-main", "pbr-main",
+ light_output, "SSR");
+ light_output = "SSR";
+ }
+
+ if (config.postaa_type == PostAAType::TAA_Low ||
+ config.postaa_type == PostAAType::TAA_Medium ||
+ config.postaa_type == PostAAType::TAA_High ||
+ config.postaa_type == PostAAType::TAA_FSR2)
+ {
+ add_mv_pass("main", "depth-main", config.postaa_type == PostAAType::TAA_FSR2);
+ }
+
+ culling_passes_info.contexts = &context;
+ culling_passes_info.num_contexts = 1;
+ culling_passes_info.tag = "main";
+ culling_passes_info.force_visible_phase2 = true;
+ hiz_main = &setup_culling_passes(graph, culling_passes_info);
+
+ if (config.hdr_bloom || hdr10)
+ {
+ bool resolved = setup_before_post_chain_antialiasing(config.postaa_type, graph, jitter, context, config.resolution_scale,
+ light_output, "depth-main", "mv-main", "HDR-resolved");
+
+ if (!hdr10)
+ {
+ HDROptions opts;
+ opts.dynamic_exposure = config.hdr_bloom_dynamic_exposure;
+
+ if (ImplementationQuirks::get().use_async_compute_post)
+ {
+ setup_hdr_postprocess_compute(graph, context.get_frame_parameters(),
+ resolved ? "HDR-resolved" : light_output, "tonemapped", opts);
+ }
+ else
+ {
+ setup_hdr_postprocess(graph, context.get_frame_parameters(), resolved ? "HDR-resolved" : light_output,
+ "tonemapped", opts);
+ }
+
+ ui_source = "tonemapped";
+ }
+ else
+ {
+ ui_source = "HDR-resolved";
+ }
+ }
+
+ if (setup_after_post_chain_antialiasing(config.postaa_type, graph, jitter, config.resolution_scale,
+ ui_source, "depth-main", "post-aa-output"))
+ {
+ ui_source = "post-aa-output";
+ }
+
+ if (config.resolution_scale < 1.0f && config.postaa_type != PostAAType::TAA_FSR2 &&
+ setup_after_post_chain_upscaling(graph, ui_source, "post-scale-output",
+ config.resolution_scale_sharpen))
+ {
+ ui_source = "post-scale-output";
+ }
+
+ if (config.show_ui)
+ {
+ auto &ui = graph.add_pass("ui", config.hdr_bloom || config.postaa_type != PostAAType::None ?
+ RenderGraph::get_default_post_graphics_queue() :
+ RENDER_GRAPH_QUEUE_GRAPHICS_BIT);
+ AttachmentInfo ui_info;
+
+ if (hdr10)
+ {
+ ui.add_color_output("ui-temporary", ui_info, ui_source);
+ ui_info.format = VK_FORMAT_R8G8B8A8_SRGB;
+
+ // TODO: Make this dynamic.
+ HDR10PQEncodingConfig hdr10_config = {};
+ hdr10_config.hdr_pre_exposure = 500.0f;
+ hdr10_config.ui_pre_exposure = 400.0f;
+ setup_hdr10_pq_encoding(graph, "ui-output", ui_source, "ui-temporary", hdr10_config,
+ get_wsi().get_hdr_metadata());
+ }
+ else
+ {
+ ui_info.flags |= ATTACHMENT_INFO_SUPPORTS_PREROTATE_BIT;
+ ui.add_color_output("ui-output", ui_info, ui_source);
+ }
+
+ graph.set_backbuffer_source("ui-output");
+
+ ui.set_get_clear_color([](unsigned, VkClearColorValue *value) {
+ value->float32[0] = 0.0f;
+ value->float32[1] = 0.0f;
+ value->float32[2] = 0.0f;
+ value->float32[3] = 1.0f;
+ return true;
+ });
+
+ ui.set_build_render_pass([this](CommandBuffer &cmd) { render_ui(cmd); });
+ }
+ else
+ graph.set_backbuffer_source(ui_source);
+
+ scene_loader.get_scene().add_render_pass_dependencies(graph);
+ graph.bake();
+#ifdef VULKAN_DEBUG
+ graph.log();
+#endif
+ graph.install_physical_buffers(std::move(physical_buffers));
+
+ need_shadow_map_update = true;
+}
+
+void SceneViewerApplication::on_swapchain_changed(const Vulkan::SwapchainParameterEvent &e)
+{
+ pending_swapchain = &e;
+}
+
+void SceneViewerApplication::on_swapchain_destroyed(const SwapchainParameterEvent &)
+{
+ pending_swapchain = nullptr;
+}
+
+void SceneViewerApplication::update_shadow_scene_aabb()
+{
+ // Get the scene AABB for shadow casters.
+ auto &scene = scene_loader.get_scene();
+ auto &shadow_casters =
+ scene.get_entity_pool()
+ .get_component_group();
+ AABB aabb(vec3(FLT_MAX), vec3(-FLT_MAX));
+ for (auto &caster : shadow_casters)
+ aabb.expand(get_component(caster)->get_aabb());
+ shadow_scene_aabb = aabb;
+}
+
+void SceneViewerApplication::setup_shadow_map()
+{
+ mat4 view = mat4_cast(look_at(-selected_directional->direction, vec3(0.0f, 1.0f, 0.0f)));
+ AABB ortho_range_depth = shadow_scene_aabb.transform(view);
+
+ // For fallback context, we render the entire scene bounds.
+ // This is not going to work well for large scenes, but oh well.
+ fallback_depth_context.set_camera(ortho(ortho_range_depth), view);
+ fallback_lighting.shadow.transforms[0] =
+ translate(vec3(0.5f, 0.5f, 0.0f)) *
+ scale(vec3(0.5f, 0.5f, 1.0f)) *
+ fallback_depth_context.get_render_parameters().view_projection;
+
+ // Project the scene AABB into the light and find our ortho ranges.
+ // This will serve as the culling bounding box.
+ // TODO: Make configurable.
+ constexpr float FIRST_SLICE_CUTOFF = 10.0f;
+ constexpr float BEGIN_LERP_FRACT = 0.8f;
+
+ const float cascade_log_bias = 1.0f - muglm::log2(FIRST_SLICE_CUTOFF);
+ const auto compute_z = [&](float slice) -> float {
+ float slice_z = FIRST_SLICE_CUTOFF * muglm::exp2(slice - 1.0f);
+ return slice_z;
+ };
+
+ lighting.shadow.cascade_log_bias = cascade_log_bias;
+
+ if (config.directional_light_cascaded_shadows)
+ {
+ for (int i = 0; i < NumShadowCascades; i++)
+ {
+ float cascade_cutoffs_lo;
+ float cascade_cutoffs_hi = compute_z(float(i + 1));
+ if (i == 0)
+ cascade_cutoffs_lo = 0.0001f;
+ else
+ cascade_cutoffs_lo = compute_z(float(i - 1) + BEGIN_LERP_FRACT);
+
+ auto near_camera = *selected_camera;
+ near_camera.set_depth_range(cascade_cutoffs_lo, cascade_cutoffs_hi);
+ vec4 sphere = Frustum::get_bounding_sphere(inverse(near_camera.get_projection()), inverse(near_camera.get_view()));
+ vec2 center_xy = (view * vec4(sphere.xyz(), 1.0f)).xy();
+ sphere.w *= 1.01f;
+
+ vec2 texel_size = vec2(2.0f * sphere.w) * vec2(1.0f / lighting.shadows->get_image().get_create_info().width,
+ 1.0f / lighting.shadows->get_image().get_create_info().height);
+
+ // Snap to texel grid.
+ center_xy = round(center_xy / texel_size) * texel_size;
+
+ AABB ortho_range = AABB(vec3(center_xy - vec2(sphere.w), ortho_range_depth.get_minimum().z),
+ vec3(center_xy + vec2(sphere.w), ortho_range_depth.get_maximum().z));
+
+ mat4 proj = ortho(ortho_range);
+ mat4 cascade_transform = proj * view;
+ lighting.shadow.transforms[i] =
+ translate(vec3(0.5f, 0.5f, 0.0f)) *
+ scale(vec3(0.5f, 0.5f, 1.0f)) *
+ cascade_transform;
+
+ depth_contexts[i].set_camera(proj, view);
+ }
+ }
+ else
+ {
+ mat4 proj = ortho(ortho_range_depth);
+ depth_contexts[0].set_camera(proj, view);
+ lighting.shadow.transforms[0] =
+ translate(vec3(0.5f, 0.5f, 0.0f)) *
+ scale(vec3(0.5f, 0.5f, 1.0f)) *
+ proj * view;
+ }
+}
+
+void SceneViewerApplication::update_scene(TaskComposer &composer, double frame_time, double elapsed_time)
+{
+ auto &scene = scene_loader.get_scene();
+
+ animation_system->animate(composer, frame_time, elapsed_time);
+ scene.update_transform_tree(composer);
+
+ constexpr unsigned NumTasks = 8;
+ Threaded::scene_update_cached_transforms(scene, composer, NumTasks);
+
+ // Perform updates which depend on node transforms.
+ auto &updates = composer.begin_pipeline_stage();
+ updates.set_desc("scene-updates");
+ updates.enqueue_task([this, need_update = need_shadow_map_update]() {
+ jitter.step(selected_camera->get_projection(), selected_camera->get_view());
+ context.set_camera(jitter.get_jittered_projection(), selected_camera->get_view());
+ context.set_motion_vector_projections(jitter);
+
+ lighting.directional.direction = selected_directional->direction;
+ lighting.directional.color = selected_directional->color;
+
+ if (lighting.shadows)
+ {
+ if (need_update)
+ update_shadow_scene_aabb();
+ setup_shadow_map();
+ }
+ });
+
+ need_shadow_map_update = false;
+ scene.refresh_per_frame(context, composer);
+}
+
+void SceneViewerApplication::render_ui(CommandBuffer &cmd)
+{
+ auto &device = cmd.get_device();
+ flat_renderer.begin();
+
+ unsigned count = std::min(last_frame_index, FrameWindowSize);
+ float total_time = 0.0f;
+ float min_time = FLT_MAX;
+ float max_time = 0.0f;
+ for (unsigned i = 0; i < count; i++)
+ {
+ total_time += last_frame_times[i];
+ min_time = std::min(min_time, last_frame_times[i]);
+ max_time = std::max(max_time, last_frame_times[i]);
+ }
+
+ char avg_text[64];
+ sprintf(avg_text, "Frame: %10.3f ms", (total_time / count) * 1000.0f);
+
+ char min_text[64];
+ sprintf(min_text, "Min: %10.3f ms", min_time * 1000.0f);
+
+ char max_text[64];
+ sprintf(max_text, "Max: %10.3f ms", max_time * 1000.0f);
+
+ char colorspace_text[64];
+ sprintf(colorspace_text, "%s",
+ get_wsi().get_backbuffer_color_space() == VK_COLOR_SPACE_HDR10_ST2084_EXT ? "ST.2084 / PQ" : "sRGB");
+
+ char pos_text[256];
+ char rot_text[256];
+ char tex_text[256];
+ auto cam_pos = selected_camera->get_position();
+ auto cam_ori = selected_camera->get_rotation();
+ snprintf(pos_text, sizeof(pos_text), "Pos: %.3f, %.3f, %.3f", cam_pos.x, cam_pos.y, cam_pos.z);
+ snprintf(rot_text, sizeof(rot_text), "Rot: %.3f, %.3f, %.3f, %.3f", cam_ori.x, cam_ori.y, cam_ori.z, cam_ori.w);
+ snprintf(tex_text, sizeof(tex_text), "Texture: %u MiB",
+ unsigned(GRANITE_ASSET_MANAGER()->get_current_total_consumed() / (1024 * 1024)));
+
+ vec3 offset(5.0f, 5.0f, 0.0f);
+ vec2 size(cmd.get_viewport().width - 10.0f, cmd.get_viewport().height - 10.0f);
+ vec4 color(1.0f, 1.0f, 0.0f, 1.0f);
+ Font::Alignment alignment = Font::Alignment::TopRight;
+
+ flat_renderer.render_text(GRANITE_UI_MANAGER()->get_font(UI::FontSize::Large), avg_text, offset, size, color,
+ alignment);
+ flat_renderer.render_text(GRANITE_UI_MANAGER()->get_font(UI::FontSize::Large), min_text,
+ offset + vec3(0.0f, 20.0f, 0.0f), size, color, alignment);
+ flat_renderer.render_text(GRANITE_UI_MANAGER()->get_font(UI::FontSize::Large), max_text,
+ offset + vec3(0.0f, 40.0f, 0.0f), size, color, alignment);
+ flat_renderer.render_text(GRANITE_UI_MANAGER()->get_font(UI::FontSize::Normal), colorspace_text,
+ offset + vec3(0.0f, 65.0f, 0.0f), size, color, alignment);
+ flat_renderer.render_text(GRANITE_UI_MANAGER()->get_font(UI::FontSize::Normal), pos_text,
+ offset + vec3(0.0f, 80.0f, 0.0f), size, color, alignment);
+ flat_renderer.render_text(GRANITE_UI_MANAGER()->get_font(UI::FontSize::Normal), rot_text,
+ offset + vec3(0.0f, 95.0f, 0.0f), size, color, alignment);
+ flat_renderer.render_text(GRANITE_UI_MANAGER()->get_font(UI::FontSize::Normal), tex_text,
+ offset + vec3(0.0f, 110.0f, 0.0f), size, color, alignment);
+
+ HeapBudget budgets[VK_MAX_MEMORY_HEAPS];
+ device.get_memory_budget(budgets);
+ for (uint32_t i = 0; i < device.get_memory_properties().memoryHeapCount; i++)
+ {
+ char heap_text[256];
+ sprintf(heap_text, "Heap #%u: (%.1f MiB / %.1f MiB) [%.1f / %.1f]", i,
+ double(budgets[i].device_usage) / double(1024 * 1024),
+ double(budgets[i].budget_size) / double(1024 * 1024),
+ double(budgets[i].tracked_usage) / double(1024 * 1024),
+ double(budgets[i].max_size) / double(1024 * 1024));
+ flat_renderer.render_text(GRANITE_UI_MANAGER()->get_font(UI::FontSize::Normal), heap_text,
+ offset + vec3(0.0f, 145.0f + 15.0f * float(i), 0.0f),
+ size, color, alignment);
+ }
+
+ flat_renderer.flush(cmd, vec3(0.0f), vec3(cmd.get_viewport().width, cmd.get_viewport().height, 1.0f));
+}
+
+void SceneViewerApplication::render_scene(TaskComposer &composer)
+{
+ auto &wsi = get_wsi();
+ auto &device = wsi.get_device();
+ graph.enqueue_render_passes(device, composer);
+}
+
+void SceneViewerApplication::post_frame()
+{
+ Application::post_frame();
+ scene_loader.get_scene().destroy_queued_entities();
+}
+
+void SceneViewerApplication::render_frame(double frame_time, double elapsed_time)
+{
+ TaskComposer composer(*GRANITE_THREAD_GROUP());
+
+ if (pending_swapchain)
+ {
+ GRANITE_SCOPED_TIMELINE_EVENT("bake-render-graph");
+ bake_render_graph(*pending_swapchain);
+ pending_swapchain = nullptr;
+ }
+
+ FrameParameters frame;
+ frame.elapsed_time = elapsed_time;
+ frame.frame_time = frame_time;
+ context.set_frame_parameters(frame);
+
+ // Set up handles before we kick of task graph.
+ auto &device = get_wsi().get_device();
+ auto &scene = scene_loader.get_scene();
+
+#if 0
+ // Hack to test per-object motion easily.
+ auto &t = scene.get_root_node()->get_transform();
+ t.translation.x = std::sin(elapsed_time);
+ scene.get_root_node()->invalidate_cached_transform();
+#endif
+
+ last_frame_times[last_frame_index++ & FrameWindowSizeMask] = float(frame_time);
+
+ graph.setup_attachments(device, &device.get_swapchain_view());
+ lighting.shadows = graph.maybe_get_physical_texture_resource(shadows);
+ lighting.ambient_occlusion = graph.maybe_get_physical_texture_resource(ssao_output);
+ context.set_scene_hiz_view(graph.maybe_get_physical_texture_resource(hiz_main), 1);
+
+ // Somewhat crude.
+ if (auto *hiz = graph.maybe_get_physical_texture_resource(hiz_depth))
+ {
+ hiz_depth_peel.clear();
+ for (unsigned layer = 0; layer < hiz->get_create_info().layers; layer++)
+ {
+ auto info = hiz->get_create_info();
+ info.view_type = VK_IMAGE_VIEW_TYPE_2D;
+ info.layers = 1;
+ info.base_layer = layer;
+ hiz_depth_peel.emplace_back(device.create_image_view(info));
+ depth_contexts[layer].set_scene_hiz_view(hiz_depth_peel.back().get(), 1);
+ }
+ }
+
+ scene_loader.get_scene().set_render_pass_data(&renderer_suite, &context);
+ scene.bind_render_graph_resources(graph);
+ renderer_suite.update_mesh_rendering_options(context, renderer_suite_config);
+
+ fallback_lighting.shadows = graph.maybe_get_physical_texture_resource(fallback_shadows);
+ fallback_lighting.directional = lighting.directional;
+ fallback_lighting.cluster = lighting.cluster;
+ fallback_lighting.volumetric_diffuse = lighting.volumetric_diffuse;
+
+ {
+ GRANITE_SCOPED_TIMELINE_EVENT("update-scene-enqueue");
+ update_scene(composer, frame_time, elapsed_time);
+ }
+
+ {
+ GRANITE_SCOPED_TIMELINE_EVENT("render-scene-enqueue");
+ render_scene(composer);
+ }
+
+ GRANITE_SCOPED_TIMELINE_EVENT("render-scene-wait");
+ auto final = composer.get_outgoing_task();
+ final->wait();
+}
+
+std::string SceneViewerApplication::get_name()
+{
+ return "scene-viewer";
+}
+
+} // namespace Granite
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/application/scene_viewer_application.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/scene_viewer_application.hpp
new file mode 100644
index 00000000..4b84db1b
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/application/scene_viewer_application.hpp
@@ -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 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 cluster;
+ std::unique_ptr volumetric_fog;
+ std::unique_ptr 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 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 hiz_depth_peel;
+
+ CullingPassesInfo culling_passes_info = {};
+};
+}
\ No newline at end of file
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/CMakeLists.txt b/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/CMakeLists.txt
new file mode 100644
index 00000000..ab31bd19
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/CMakeLists.txt
@@ -0,0 +1,18 @@
+add_granite_internal_lib(granite-compiler compiler.cpp compiler.hpp)
+target_include_directories(granite-compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_link_libraries(granite-compiler
+ PUBLIC granite-application-global
+ PRIVATE SPIRV-Tools shaderc granite-path granite-util)
+
+if (GRANITE_SHADER_COMPILER_OPTIMIZE)
+ target_compile_definitions(granite-compiler PRIVATE GRANITE_COMPILER_OPTIMIZE=1)
+else()
+ target_compile_definitions(granite-compiler PRIVATE GRANITE_COMPILER_OPTIMIZE=0)
+endif()
+
+if (${CMAKE_BUILD_TYPE} MATCHES "Debug")
+ target_compile_definitions(granite-compiler PRIVATE GRANITE_COMPILER_DEBUG=1)
+else()
+ target_compile_definitions(granite-compiler PRIVATE GRANITE_COMPILER_DEBUG=0)
+endif()
+
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/compiler.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/compiler.cpp
new file mode 100644
index 00000000..e4ba8079
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/compiler.cpp
@@ -0,0 +1,381 @@
+/* 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 "compiler.hpp"
+#include "shaderc/shaderc.hpp"
+#include "path_utils.hpp"
+#include "logging.hpp"
+#include "string_helpers.hpp"
+
+#include "spirv-tools/libspirv.hpp"
+
+namespace Granite
+{
+GLSLCompiler::GLSLCompiler(FilesystemInterface &iface_)
+ : iface(iface_)
+{
+}
+
+static Stage stage_from_path(const std::string &path)
+{
+ auto ext = Path::ext(path);
+ if (ext == "vert")
+ return Stage::Vertex;
+ else if (ext == "frag")
+ return Stage::Fragment;
+ else if (ext == "comp")
+ return Stage::Compute;
+ else if (ext == "task")
+ return Stage::Task;
+ else if (ext == "mesh")
+ return Stage::Mesh;
+ else
+ return Stage::Unknown;
+}
+
+static Stage convert_stage(const std::string &stage)
+{
+ if (stage == "vertex")
+ return Stage::Vertex;
+ else if (stage == "compute")
+ return Stage::Compute;
+ else if (stage == "fragment")
+ return Stage::Fragment;
+ else if (stage == "task")
+ return Stage::Task;
+ else if (stage == "mesh")
+ return Stage::Mesh;
+ else
+ return Stage::Unknown;
+}
+
+bool GLSLCompiler::set_source_from_file(const std::string &path, Stage forced_stage)
+{
+ if (!iface.load_text_file(path, source))
+ {
+ LOGE("Failed to load shader: %s\n", path.c_str());
+ return false;
+ }
+
+ source_path = path;
+
+ if (forced_stage != Stage::Unknown)
+ stage = forced_stage;
+ else
+ stage = stage_from_path(path);
+
+ return stage != Stage::Unknown;
+}
+
+bool GLSLCompiler::set_source_from_file_multistage(const std::string &path)
+{
+ if (iface.load_text_file(path, source))
+ {
+ LOGE("Failed to load shader: %s\n", path.c_str());
+ return false;
+ }
+
+ source_path = path;
+ stage = Stage::Unknown;
+ return true;
+}
+
+void GLSLCompiler::set_include_directories(const std::vector *include_directories_)
+{
+ include_directories = include_directories_;
+}
+
+bool GLSLCompiler::find_include_path(const std::string &source_path_, const std::string &include_path,
+ std::string &included_path, std::string &included_source)
+{
+ auto relpath = Path::relpath(source_path_, include_path);
+ if (iface.load_text_file(relpath, included_source))
+ {
+ included_path = relpath;
+ return true;
+ }
+
+ if (include_directories)
+ {
+ for (auto &include_dir : *include_directories)
+ {
+ auto path = Path::join(include_dir, include_path);
+ if (iface.load_text_file(path, included_source))
+ {
+ included_path = path;
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool GLSLCompiler::parse_variants(const std::string &source_, const std::string &path)
+{
+ auto lines = Util::split(source_, "\n");
+
+ unsigned line_index = 1;
+ size_t offset;
+
+ for (auto &line : lines)
+ {
+ // This check, followed by the include statement check below isn't even remotely correct,
+ // but we only have to care about shaders we control here.
+ if ((offset = line.find("//")) != std::string::npos)
+ line = line.substr(0, offset);
+
+ if ((offset = line.find("#include \"")) != std::string::npos)
+ {
+ auto include_path = line.substr(offset + 10);
+ if (!include_path.empty() && include_path.back() == '"')
+ include_path.pop_back();
+
+ std::string included_source;
+ if (!find_include_path(path, include_path, include_path, included_source))
+ {
+ LOGE("Failed to include GLSL file: %s\n", include_path.c_str());
+ return false;
+ }
+
+ preprocessed_source += Util::join("#line ", 1, " \"", include_path, "\"\n");
+ if (!parse_variants(included_source, include_path))
+ return false;
+ preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
+
+ dependencies.insert(include_path);
+ }
+ else if (line.find("#pragma optimize off") == 0)
+ {
+ optimization = Optimization::ForceOff;
+ preprocessed_source += "// #pragma optimize off";
+ preprocessed_source += '\n';
+ }
+ else if (line.find("#pragma optimize on") == 0)
+ {
+ optimization = Optimization::ForceOn;
+ preprocessed_source += "// #pragma optimize on";
+ preprocessed_source += '\n';
+ }
+ else if (line.find("#pragma stage ") == 0)
+ {
+ if (!preprocessed_source.empty())
+ {
+ preprocessed_sections.push_back({ preprocessing_active_stage, std::move(preprocessed_source) });
+ preprocessed_source = {};
+ }
+ preprocessing_active_stage = convert_stage(line.substr(14));
+ preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
+ }
+ else if (line.find("#pragma ") == 0)
+ {
+ pragmas.push_back(line.substr(8));
+ preprocessed_source += "// ";
+ preprocessed_source += line;
+ preprocessed_source += '\n';
+ }
+ else
+ {
+ preprocessed_source += line;
+ preprocessed_source += '\n';
+
+ auto first_non_space = line.find_first_not_of(' ');
+ if (first_non_space != std::string::npos && line[first_non_space] == '#')
+ {
+ auto keywords = Util::split(line.substr(first_non_space + 1), " ");
+ if (keywords.size() == 1)
+ {
+ auto &word = keywords.front();
+ if (word == "endif")
+ preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n");
+ }
+ }
+ }
+
+ line_index++;
+ }
+ return true;
+}
+
+bool GLSLCompiler::preprocess()
+{
+ // Use a custom preprocessor where we only resolve includes once.
+ // The builtin glslang preprocessor is not suitable for this task,
+ // since we need to defer resolving defines.
+
+ preprocessed_source.clear();
+ pragmas.clear();
+ preprocessed_sections.clear();
+ preprocessing_active_stage = Stage::Unknown;
+
+ bool ret = parse_variants(source, source_path);
+
+ if (ret && !preprocessed_source.empty())
+ {
+ preprocessed_sections.push_back({ preprocessing_active_stage, std::move(preprocessed_source) });
+ preprocessed_source = {};
+ }
+
+ return ret;
+}
+
+Util::Hash GLSLCompiler::get_source_hash() const
+{
+ Util::Hasher h;
+ for (auto §ion : preprocessed_sections)
+ {
+ h.u32(uint32_t(section.stage));
+ h.string(section.source);
+ }
+ h.string(preprocessed_source);
+ return h.get();
+}
+
+std::vector GLSLCompiler::compile(std::string &error_message, const std::vector> *defines) const
+{
+ shaderc::Compiler compiler;
+ shaderc::CompileOptions options;
+ if (preprocessed_sections.empty())
+ {
+ error_message = "Need to preprocess source first.";
+ return {};
+ }
+
+ if (defines)
+ for (auto &define : *defines)
+ options.AddMacroDefinition(define.first, std::to_string(define.second));
+
+#if GRANITE_COMPILER_OPTIMIZE
+ if (optimization != Optimization::ForceOff)
+ options.SetOptimizationLevel(shaderc_optimization_level_performance);
+ else
+ options.SetOptimizationLevel(shaderc_optimization_level_zero);
+#else
+ options.SetOptimizationLevel(shaderc_optimization_level_zero);
+#endif
+
+ if (!strip && (GRANITE_COMPILER_DEBUG || optimization == Optimization::ForceOff))
+ options.SetGenerateDebugInfo();
+
+ shaderc_shader_kind kind;
+ switch (stage)
+ {
+ case Stage::Vertex:
+ kind = shaderc_glsl_vertex_shader;
+ options.AddMacroDefinition("STAGE_VERTEX", "1");
+ break;
+
+ case Stage::Fragment:
+ kind = shaderc_glsl_fragment_shader;
+ options.AddMacroDefinition("STAGE_FRAGMENT", "1");
+ break;
+
+ case Stage::Compute:
+ kind = shaderc_glsl_compute_shader;
+ options.AddMacroDefinition("STAGE_COMPUTE", "1");
+ break;
+
+ case Stage::Task:
+ kind = shaderc_glsl_task_shader;
+ options.AddMacroDefinition("STAGE_TASK", "1");
+ break;
+
+ case Stage::Mesh:
+ kind = shaderc_glsl_mesh_shader;
+ options.AddMacroDefinition("STAGE_MESH", "1");
+ break;
+
+ default:
+ error_message = "Invalid shader stage.";
+ return {};
+ }
+
+ auto env = shaderc_env_version_vulkan_1_1;
+ auto spv_version = shaderc_spirv_version_1_3;
+
+ if (target == Target::Vulkan12)
+ {
+ env = shaderc_env_version_vulkan_1_2;
+ spv_version = shaderc_spirv_version_1_5;
+ }
+ else if (target == Target::Vulkan13)
+ {
+ env = shaderc_env_version_vulkan_1_3;
+ spv_version = shaderc_spirv_version_1_6;
+ }
+
+ options.SetTargetEnvironment(shaderc_target_env_vulkan, env);
+ options.SetTargetSpirv(spv_version);
+ options.SetSourceLanguage(shaderc_source_language_glsl);
+
+ shaderc::SpvCompilationResult result;
+
+ if (preprocessed_sections.size() == 1)
+ {
+ if (preprocessed_sections.front().stage != Stage::Unknown && preprocessed_sections.front().stage != stage)
+ {
+ error_message = "No preprocessed sections available.";
+ return {};
+ }
+ result = compiler.CompileGlslToSpv(preprocessed_sections.front().source, kind, source_path.c_str(), options);
+ }
+ else
+ {
+ std::string combined_source;
+ for (auto §ion : preprocessed_sections)
+ if (section.stage == Stage::Unknown || section.stage == stage)
+ combined_source += section.source;
+ if (combined_source.empty())
+ {
+ error_message = "No preprocessed sections available.";
+ return {};
+ }
+ result = compiler.CompileGlslToSpv(combined_source, kind, source_path.c_str(), options);
+ }
+
+ error_message.clear();
+ if (result.GetCompilationStatus() != shaderc_compilation_status_success)
+ {
+ error_message = result.GetErrorMessage();
+ return {};
+ }
+
+ std::vector compiled_spirv(result.cbegin(), result.cend());
+
+#if 0
+ spvtools::SpirvTools core(target == Target::Vulkan13 ? SPV_ENV_VULKAN_1_3 : SPV_ENV_VULKAN_1_1);
+ core.SetMessageConsumer([&error_message](spv_message_level_t, const char *, const spv_position_t&, const char *message) {
+ error_message = message;
+ });
+
+ spvtools::ValidatorOptions opts;
+ opts.SetScalarBlockLayout(true);
+ if (!core.Validate(compiled_spirv.data(), compiled_spirv.size(), opts))
+ {
+ error_message += "\nFailed to validate SPIR-V.\n";
+ return {};
+ }
+#endif
+
+ return compiled_spirv;
+}
+}
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/compiler.hpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/compiler.hpp
new file mode 100644
index 00000000..c016ef7d
--- /dev/null
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/compiler/compiler.hpp
@@ -0,0 +1,139 @@
+/* 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
+#include
+#include
+#include
+#include "small_vector.hpp"
+#include "global_managers.hpp"
+#include "hash.hpp"
+
+namespace Granite
+{
+enum class Stage
+{
+ Vertex,
+ Fragment = 4, // Skip over tess/geom to match Vulkan ordering
+ Compute,
+ Task,
+ Mesh,
+ Unknown
+};
+
+enum class Target
+{
+ Vulkan11,
+ Vulkan12,
+ Vulkan13
+};
+
+class GLSLCompiler
+{
+public:
+ explicit GLSLCompiler(FilesystemInterface &iface);
+
+ void set_target(Target target_)
+ {
+ target = target_;
+ }
+
+ void set_stage(Stage stage_)
+ {
+ stage = stage_;
+ }
+
+ void set_source(std::string source_, std::string path)
+ {
+ source = std::move(source_);
+ source_path = std::move(path);
+ }
+
+ void set_include_directories(const std::vector *include_directories);
+
+ bool set_source_from_file(const std::string &path, Stage stage = Stage::Unknown);
+ bool set_source_from_file_multistage(const std::string &path);
+ bool preprocess();
+ Util::Hash get_source_hash() const;
+
+ std::vector compile(std::string &error_message, const std::vector> *defines = nullptr) const;
+
+ const std::unordered_set