feat(host): vendor PyroWave + minimal Granite subset as crates/pyrowave-sys
Phase 0 of design/pyrowave-codec-plan.md — the opt-in wired-LAN ultra-low- latency codec. Vendored at upstream 509e4f88 (API 0.4.0, Granite 44362775, volk + vulkan-headers pins in PUNKTFUNK-VENDOR.txt), pruned to the 6.6 MB the standalone no-renderer build needs; scripts/vendor-pyrowave.sh reproduces the tree (a pin bump is protocol-affecting, plan §4.2). build.rs drives the wrapper CMakeLists (static archives incl. a static C-API lib upstream only ships shared) + bindgen over pyrowave.h; Linux and Windows only, empty stub elsewhere (Apple gets a native Metal port, §4.7). Offline-safe by construction: no network, no system lib, vendored Vulkan headers — same model as the opus dep (flatpak builder has no network). Phase-0 validation on .21 (RTX 5070 Ti, driver 610.43.03): - upstream pyrowave-c-test + interop test (incl. dmabuf/DRM-modifier Vulkan<->Vulkan) pass, from the pristine AND the pruned tree - GPU kernel times at ~1.6 bpp noise: encode/decode 0.090/0.042 ms @800p, 0.146/0.067 @1080p, 0.226/0.103 @1440p, 0.477/0.201 @4K — order of magnitude under NVENC's 1-2 ms retrieve, CBR lands within ~100 B of target - cargo test -p pyrowave-sys green (static link + API-version pin check) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+8
@@ -3190,6 +3190,14 @@ dependencies = [
|
|||||||
"winresource",
|
"winresource",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyrowave-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cmake",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "1.2.3"
|
version = "1.2.3"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ members = [
|
|||||||
"crates/pf-console-ui",
|
"crates/pf-console-ui",
|
||||||
"crates/pf-ffvk",
|
"crates/pf-ffvk",
|
||||||
"crates/pf-driver-proto",
|
"crates/pf-driver-proto",
|
||||||
|
"crates/pyrowave-sys",
|
||||||
"clients/probe",
|
"clients/probe",
|
||||||
"clients/linux",
|
"clients/linux",
|
||||||
"clients/session",
|
"clients/session",
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 }
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+173
@@ -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()
|
||||||
|
|
||||||
+167
@@ -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<A<int> > instead of A<A<int>> 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
|
||||||
@@ -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
|
||||||
@@ -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($<$<AND:$<PLATFORM_ID:Android>,$<CONFIG:RELEASE>>:-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()
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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`)
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
add_granite_internal_lib(granite-application
|
||||||
|
application.hpp
|
||||||
|
application_glue.hpp
|
||||||
|
application.cpp)
|
||||||
|
|
||||||
|
if (GRANITE_FFMPEG)
|
||||||
|
target_link_libraries(granite-application PRIVATE granite-video)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(granite-application PUBLIC
|
||||||
|
granite-vulkan
|
||||||
|
granite-event
|
||||||
|
granite-input
|
||||||
|
granite-application-global-init
|
||||||
|
granite-application-events
|
||||||
|
granite-threading
|
||||||
|
granite-filesystem)
|
||||||
|
|
||||||
|
if (NOT (${GRANITE_PLATFORM} MATCHES "null"))
|
||||||
|
target_sources(granite-application PRIVATE platforms/application_headless.cpp)
|
||||||
|
target_link_libraries(granite-application PRIVATE granite-stb granite-rapidjson)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_include_directories(granite-application PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
|
if (TARGET granite-renderer)
|
||||||
|
target_link_libraries(granite-application PUBLIC granite-renderer PRIVATE granite-ui)
|
||||||
|
target_compile_definitions(granite-application PRIVATE HAVE_GRANITE_RENDERER)
|
||||||
|
|
||||||
|
add_granite_internal_lib(granite-scene-viewer scene_viewer_application.cpp scene_viewer_application.hpp)
|
||||||
|
target_include_directories(granite-scene-viewer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(granite-scene-viewer
|
||||||
|
PUBLIC granite-scene-export granite-ui granite-application
|
||||||
|
PRIVATE granite-rapidjson)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Can be defined by application to get a custom entry point.
|
||||||
|
# Otherwise, get a default one.
|
||||||
|
if (NOT TARGET granite-application-interface-query)
|
||||||
|
add_granite_internal_lib(granite-application-interface-query STATIC application_interface_query.cpp)
|
||||||
|
target_compile_options(granite-application-interface-query PRIVATE ${GRANITE_CXX_FLAGS})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT ANDROID)
|
||||||
|
add_granite_internal_lib(granite-application-entry STATIC application_entry.cpp)
|
||||||
|
target_compile_options(granite-application-entry PRIVATE ${GRANITE_CXX_FLAGS})
|
||||||
|
target_link_libraries(granite-application-entry
|
||||||
|
PRIVATE granite-application-interface-query granite-application granite-platform granite-filesystem)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(granite-application-entry-headless STATIC application_entry.cpp)
|
||||||
|
target_compile_definitions(granite-application-entry-headless PRIVATE APPLICATION_ENTRY_HEADLESS=1)
|
||||||
|
target_link_libraries(granite-application-entry-headless
|
||||||
|
PRIVATE granite-application-interface-query granite-application granite-platform granite-filesystem)
|
||||||
|
target_compile_options(granite-application-entry-headless PRIVATE ${GRANITE_CXX_FLAGS})
|
||||||
|
|
||||||
|
add_subdirectory(events)
|
||||||
|
add_subdirectory(input)
|
||||||
|
add_subdirectory(platforms)
|
||||||
|
add_subdirectory(global)
|
||||||
|
|
||||||
|
if (GRANITE_AUDIO)
|
||||||
|
target_link_libraries(granite-application PRIVATE granite-audio)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define NOMINMAX
|
||||||
|
#include "application.hpp"
|
||||||
|
#include "asset_manager.hpp"
|
||||||
|
#include "thread_group.hpp"
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
#include "material_manager.hpp"
|
||||||
|
#include "common_renderer_data.hpp"
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
#include "audio_mixer.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace Vulkan;
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
Application::~Application()
|
||||||
|
{
|
||||||
|
auto *group = GRANITE_THREAD_GROUP();
|
||||||
|
if (group)
|
||||||
|
group->wait_idle();
|
||||||
|
|
||||||
|
teardown_wsi();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Application::init_platform(std::unique_ptr<WSIPlatform> new_platform)
|
||||||
|
{
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
if (auto *common = GRANITE_COMMON_RENDERER_DATA())
|
||||||
|
common->initialize_static_assets(GRANITE_ASSET_MANAGER(), GRANITE_FILESYSTEM());
|
||||||
|
#endif
|
||||||
|
platform = std::move(new_platform);
|
||||||
|
application_wsi.set_platform(platform.get());
|
||||||
|
|
||||||
|
if (auto *event = GRANITE_EVENT_MANAGER())
|
||||||
|
event->enqueue_latched<ApplicationWSIPlatformEvent>(*platform);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::teardown_wsi()
|
||||||
|
{
|
||||||
|
if (auto *event = GRANITE_EVENT_MANAGER())
|
||||||
|
{
|
||||||
|
event->dequeue_all_latched(DevicePipelineReadyEvent::get_type_id());
|
||||||
|
event->dequeue_all_latched(DeviceShaderModuleReadyEvent::get_type_id());
|
||||||
|
event->dequeue_all_latched(ApplicationWSIPlatformEvent::get_type_id());
|
||||||
|
}
|
||||||
|
application_wsi.teardown();
|
||||||
|
ready_modules = false;
|
||||||
|
ready_pipelines = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Application::init_wsi(Vulkan::ContextHandle context)
|
||||||
|
{
|
||||||
|
if (context)
|
||||||
|
{
|
||||||
|
if (!application_wsi.init_from_existing_context(std::move(context)))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Context::SystemHandles system_handles;
|
||||||
|
system_handles.filesystem = GRANITE_FILESYSTEM();
|
||||||
|
system_handles.thread_group = GRANITE_THREAD_GROUP();
|
||||||
|
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
|
||||||
|
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
|
||||||
|
|
||||||
|
if (!application_wsi.init_context_from_platform(
|
||||||
|
system_handles.thread_group->get_num_threads() + 1,
|
||||||
|
system_handles))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!application_wsi.init_device())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
application_wsi.get_device().begin_shader_caches();
|
||||||
|
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("wsi-init-swapchain");
|
||||||
|
if (!platform->has_external_swapchain() && !application_wsi.init_surface_swapchain())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::poll_input_tracker_async(Granite::InputTrackerHandler *override_handler)
|
||||||
|
{
|
||||||
|
get_platform().poll_input_async(override_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Application::poll()
|
||||||
|
{
|
||||||
|
auto &wsi = get_wsi();
|
||||||
|
if (!get_platform().alive(wsi))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (requested_shutdown)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto *fs = GRANITE_FILESYSTEM();
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (fs)
|
||||||
|
fs->poll_notifications();
|
||||||
|
if (em)
|
||||||
|
em->dispatch();
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
auto *backend = GRANITE_AUDIO_BACKEND();
|
||||||
|
if (backend)
|
||||||
|
backend->heartbeat();
|
||||||
|
auto *am = GRANITE_AUDIO_MIXER();
|
||||||
|
if (am)
|
||||||
|
{
|
||||||
|
// Pump through events from audio thread.
|
||||||
|
auto &queue = am->get_message_queue();
|
||||||
|
Util::MessageQueuePayload payload;
|
||||||
|
while ((payload = queue.read_message()))
|
||||||
|
{
|
||||||
|
auto &event = payload.as<Event>();
|
||||||
|
if (em)
|
||||||
|
em->dispatch_inline(event);
|
||||||
|
queue.recycle_payload(std::move(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recycle dead streams.
|
||||||
|
am->dispose_dead_streams();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::check_initialization_progress()
|
||||||
|
{
|
||||||
|
auto &device = get_wsi().get_device();
|
||||||
|
|
||||||
|
if (!ready_modules)
|
||||||
|
{
|
||||||
|
if (device.query_initialization_progress(Device::InitializationStage::CacheMaintenance) >= 100 &&
|
||||||
|
device.query_initialization_progress(Device::InitializationStage::ShaderModules) >= 100)
|
||||||
|
{
|
||||||
|
if (auto *manager = GRANITE_ASSET_MANAGER())
|
||||||
|
{
|
||||||
|
// Now is a good time to kick shader manager since it might require compute shaders for decode.
|
||||||
|
manager->iterate(GRANITE_THREAD_GROUP());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto *event = GRANITE_EVENT_MANAGER())
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("dispatch-ready-modules");
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
auto *manager = &device.get_shader_manager();
|
||||||
|
#else
|
||||||
|
constexpr Vulkan::ShaderManager *manager = nullptr;
|
||||||
|
#endif
|
||||||
|
event->enqueue_latched<DeviceShaderModuleReadyEvent>(&device, manager);
|
||||||
|
}
|
||||||
|
ready_modules = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready_pipelines)
|
||||||
|
{
|
||||||
|
if (device.query_initialization_progress(Device::InitializationStage::Pipelines) >= 100)
|
||||||
|
{
|
||||||
|
if (auto *event = GRANITE_EVENT_MANAGER())
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("dispatch-ready-pipelines");
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
auto *manager = &device.get_shader_manager();
|
||||||
|
#else
|
||||||
|
constexpr Vulkan::ShaderManager *manager = nullptr;
|
||||||
|
#endif
|
||||||
|
event->enqueue_latched<DevicePipelineReadyEvent>(&device, manager);
|
||||||
|
}
|
||||||
|
ready_pipelines = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::show_message_box(const std::string &str, Vulkan::WSIPlatform::MessageType type)
|
||||||
|
{
|
||||||
|
if (platform)
|
||||||
|
platform->show_message_box(str, type);
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case Vulkan::WSIPlatform::MessageType::Error:
|
||||||
|
LOGE("%s\n", str.c_str());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Vulkan::WSIPlatform::MessageType::Warning:
|
||||||
|
LOGW("%s\n", str.c_str());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Vulkan::WSIPlatform::MessageType::Info:
|
||||||
|
LOGI("%s\n", str.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::run_frame()
|
||||||
|
{
|
||||||
|
check_initialization_progress();
|
||||||
|
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("wsi-begin-frame");
|
||||||
|
if (!application_wsi.begin_frame())
|
||||||
|
{
|
||||||
|
LOGE("Failed to begin frame. Fatal error. Shutting down.\n");
|
||||||
|
request_shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double smooth_frame_time = application_wsi.get_smooth_frame_time();
|
||||||
|
double smooth_elapsed = application_wsi.get_smooth_elapsed_time();
|
||||||
|
|
||||||
|
if (!ready_modules)
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("render-early-loading");
|
||||||
|
render_early_loading(smooth_frame_time, smooth_elapsed);
|
||||||
|
}
|
||||||
|
else if (!ready_pipelines)
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("render-loading");
|
||||||
|
render_loading(smooth_frame_time, smooth_elapsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("render-frame");
|
||||||
|
render_frame(smooth_frame_time, smooth_elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("wsi-end-frame");
|
||||||
|
application_wsi.end_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("post-frame");
|
||||||
|
post_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::render_early_loading(double, double)
|
||||||
|
{
|
||||||
|
auto &device = application_wsi.get_device();
|
||||||
|
auto cmd = device.request_command_buffer();
|
||||||
|
auto rp = device.get_swapchain_render_pass(SwapchainRenderPass::ColorOnly);
|
||||||
|
rp.clear_color[0].float32[0] = 0.01f;
|
||||||
|
rp.clear_color[0].float32[2] = 0.02f;
|
||||||
|
rp.clear_color[0].float32[3] = 0.03f;
|
||||||
|
cmd->begin_render_pass(rp);
|
||||||
|
auto vp = cmd->get_viewport();
|
||||||
|
|
||||||
|
VkClearRect rect = {};
|
||||||
|
rect.layerCount = 1;
|
||||||
|
rect.rect.extent = {
|
||||||
|
uint32_t(vp.width * 0.01f * float(device.query_initialization_progress(Device::InitializationStage::ShaderModules))),
|
||||||
|
uint32_t(vp.height),
|
||||||
|
};
|
||||||
|
|
||||||
|
VkClearValue value = {};
|
||||||
|
value.color.float32[0] = 0.08f;
|
||||||
|
value.color.float32[1] = 0.01f;
|
||||||
|
value.color.float32[2] = 0.01f;
|
||||||
|
|
||||||
|
if (rect.rect.extent.width > 0)
|
||||||
|
cmd->clear_quad(0, rect, value);
|
||||||
|
|
||||||
|
cmd->end_render_pass();
|
||||||
|
device.submit(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::render_loading(double, double)
|
||||||
|
{
|
||||||
|
auto &device = application_wsi.get_device();
|
||||||
|
auto cmd = device.request_command_buffer();
|
||||||
|
auto rp = device.get_swapchain_render_pass(SwapchainRenderPass::ColorOnly);
|
||||||
|
rp.clear_color[0].float32[0] = 0.01f;
|
||||||
|
rp.clear_color[0].float32[2] = 0.02f;
|
||||||
|
rp.clear_color[0].float32[3] = 0.03f;
|
||||||
|
cmd->begin_render_pass(rp);
|
||||||
|
auto vp = cmd->get_viewport();
|
||||||
|
|
||||||
|
VkClearRect rect = {};
|
||||||
|
rect.layerCount = 1;
|
||||||
|
rect.rect.extent = {
|
||||||
|
uint32_t(vp.width * 0.01f * float(device.query_initialization_progress(Device::InitializationStage::Pipelines))),
|
||||||
|
uint32_t(vp.height),
|
||||||
|
};
|
||||||
|
|
||||||
|
VkClearValue value = {};
|
||||||
|
value.color.float32[0] = 0.01f;
|
||||||
|
value.color.float32[1] = 0.08f;
|
||||||
|
value.color.float32[2] = 0.01f;
|
||||||
|
if (rect.rect.extent.width > 0)
|
||||||
|
cmd->clear_quad(0, rect, value);
|
||||||
|
|
||||||
|
cmd->end_render_pass();
|
||||||
|
device.submit(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::post_frame()
|
||||||
|
{
|
||||||
|
// Texture manager might require shaders to be ready before we can submit work.
|
||||||
|
if (ready_modules)
|
||||||
|
{
|
||||||
|
if (auto *manager = GRANITE_ASSET_MANAGER())
|
||||||
|
manager->iterate(GRANITE_THREAD_GROUP());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto *manager = Global::material_manager())
|
||||||
|
manager->iterate(Global::asset_manager());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "wsi.hpp"
|
||||||
|
#include "application_wsi_events.hpp"
|
||||||
|
#include "input.hpp"
|
||||||
|
#include "application_glue.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class Application
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~Application();
|
||||||
|
virtual void render_frame(double frame_time, double elapsed_time) = 0;
|
||||||
|
bool init_platform(std::unique_ptr<Vulkan::WSIPlatform> platform);
|
||||||
|
bool init_wsi(Vulkan::ContextHandle context = {});
|
||||||
|
void teardown_wsi();
|
||||||
|
|
||||||
|
// Called after the frame is submitted for presentation.
|
||||||
|
// Can do "garbage collection" or similar batched cleanup
|
||||||
|
// that does not depend on submitting more graphics work.
|
||||||
|
virtual void post_frame();
|
||||||
|
|
||||||
|
// In early loading, we have not loaded SPIR-V yet.
|
||||||
|
// Rendering a background color or extremely basic shaders could work here.
|
||||||
|
// If compiling without SPIRV-Cross or compiler support in a shipping configuration,
|
||||||
|
// any SPIR-V must be provided inline through slangmosh or similar.
|
||||||
|
virtual void render_early_loading(double frame_time, double elapsed_time);
|
||||||
|
// In loading, we have access to SPIR-V, but compiling pipelines is not done yet.
|
||||||
|
// This stage is more suited for rendering splash screens or similar.
|
||||||
|
virtual void render_loading(double frame_time, double elapsed_time);
|
||||||
|
|
||||||
|
Vulkan::WSI &get_wsi()
|
||||||
|
{
|
||||||
|
return application_wsi;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vulkan::WSIPlatform &get_platform()
|
||||||
|
{
|
||||||
|
return *platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual std::string get_name()
|
||||||
|
{
|
||||||
|
return "granite";
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual unsigned get_version()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual unsigned get_default_width()
|
||||||
|
{
|
||||||
|
return 1280;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual unsigned get_default_height()
|
||||||
|
{
|
||||||
|
return 720;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool poll();
|
||||||
|
void run_frame();
|
||||||
|
void show_message_box(const std::string &str, Vulkan::WSIPlatform::MessageType type);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void request_shutdown()
|
||||||
|
{
|
||||||
|
requested_shutdown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll_input_tracker_async(InputTrackerHandler *override_handler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Vulkan::WSIPlatform> platform;
|
||||||
|
Vulkan::WSI application_wsi;
|
||||||
|
bool requested_shutdown = false;
|
||||||
|
|
||||||
|
// Ready state for deferred device initialization.
|
||||||
|
bool ready_modules = false;
|
||||||
|
bool ready_pipelines = false;
|
||||||
|
void check_initialization_progress();
|
||||||
|
};
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "application.hpp"
|
||||||
|
#include "filesystem.hpp"
|
||||||
|
#include "path_utils.hpp"
|
||||||
|
|
||||||
|
//#define USE_FP_EXCEPTIONS
|
||||||
|
#ifdef USE_FP_EXCEPTIONS
|
||||||
|
#include <fenv.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(_WIN32) && !defined(APPLICATION_ENTRY_HEADLESS)
|
||||||
|
#define USE_WINMAIN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_WINMAIN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
#include <shellapi.h>
|
||||||
|
#include <vector>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
// Make sure this is linked in.
|
||||||
|
void application_dummy()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternatively, make sure this is linked in.
|
||||||
|
// Implementation is here to trick a linker to always let main() in static library work.
|
||||||
|
void application_setup_default_filesystem(const char *default_asset_directory)
|
||||||
|
{
|
||||||
|
auto *filesystem = GRANITE_FILESYSTEM();
|
||||||
|
if (filesystem)
|
||||||
|
Filesystem::setup_default_filesystem(filesystem, default_asset_directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WINMAIN
|
||||||
|
int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||||
|
#else
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
#ifdef USE_FP_EXCEPTIONS
|
||||||
|
feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_WINMAIN
|
||||||
|
int argc;
|
||||||
|
wchar_t **wide_argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||||
|
std::vector<char *> argv_buffer(argc + 1);
|
||||||
|
char **argv = nullptr;
|
||||||
|
std::vector<std::string> argv_strings(argc);
|
||||||
|
|
||||||
|
if (wide_argv)
|
||||||
|
{
|
||||||
|
argv = argv_buffer.data();
|
||||||
|
for (int i = 0; i < argc; i++)
|
||||||
|
{
|
||||||
|
argv_strings[i] = Granite::Path::to_utf8(wide_argv[i]);
|
||||||
|
argv_buffer[i] = const_cast<char *>(argv_strings[i].c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef APPLICATION_ENTRY_HEADLESS
|
||||||
|
int ret = Granite::application_main_headless(Granite::query_application_interface,
|
||||||
|
Granite::application_create,
|
||||||
|
argc, argv);
|
||||||
|
#else
|
||||||
|
int ret = Granite::application_main(Granite::query_application_interface,
|
||||||
|
Granite::application_create,
|
||||||
|
argc, argv);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
enum class ApplicationQuery
|
||||||
|
{
|
||||||
|
DefaultManagerFlags
|
||||||
|
};
|
||||||
|
|
||||||
|
class Application;
|
||||||
|
|
||||||
|
int application_main(
|
||||||
|
bool (*query_application_interface)(ApplicationQuery, void *data, size_t size),
|
||||||
|
Application *(*create_application)(int, char **),
|
||||||
|
int argc, char **argv);
|
||||||
|
|
||||||
|
int application_main_headless(
|
||||||
|
bool (*query_application_interface)(ApplicationQuery, void *data, size_t size),
|
||||||
|
Application *(*create_application)(int, char **),
|
||||||
|
int argc, char **argv);
|
||||||
|
|
||||||
|
extern Application *application_create(int argc, char *argv[]);
|
||||||
|
|
||||||
|
struct ApplicationQueryDefaultManagerFlags
|
||||||
|
{
|
||||||
|
uint32_t manager_feature_flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern bool query_application_interface(ApplicationQuery query, void *data, size_t size);
|
||||||
|
|
||||||
|
// Call this or setup_default_filesystem to ensure application-main is linked in correctly without having to mess around
|
||||||
|
// with -Wl,--whole-archive.
|
||||||
|
void application_dummy();
|
||||||
|
|
||||||
|
void application_setup_default_filesystem(const char *default_asset_directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ASSET_DIRECTORY
|
||||||
|
#define GRANITE_APPLICATION_SETUP_FILESYSTEM() ::Granite::application_setup_default_filesystem(ASSET_DIRECTORY)
|
||||||
|
#else
|
||||||
|
#define GRANITE_APPLICATION_SETUP_FILESYSTEM() ::Granite::application_setup_default_filesystem(nullptr)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define GRANITE_APPLICATION_DECL_DEFAULT_QUERY() namespace Granite { bool query_application_interface(Granite::ApplicationQuery, void *, size_t) { return false; } }
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#include "application_glue.hpp"
|
||||||
|
GRANITE_APPLICATION_DECL_DEFAULT_QUERY()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
add_granite_internal_lib(granite-application-events
|
||||||
|
application_wsi.hpp
|
||||||
|
application_wsi.cpp application_wsi_events.hpp
|
||||||
|
application_events.hpp)
|
||||||
|
target_include_directories(granite-application-events PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(granite-application-events
|
||||||
|
PRIVATE granite-vulkan granite-event granite-input granite-application-global)
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "event.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
enum class ApplicationLifecycle
|
||||||
|
{
|
||||||
|
Running,
|
||||||
|
Paused,
|
||||||
|
Stopped,
|
||||||
|
Dead
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApplicationLifecycleEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(ApplicationLifecycleEvent)
|
||||||
|
|
||||||
|
explicit ApplicationLifecycleEvent(ApplicationLifecycle lifecycle_)
|
||||||
|
: lifecycle(lifecycle_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationLifecycle get_lifecycle() const
|
||||||
|
{
|
||||||
|
return lifecycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
ApplicationLifecycle lifecycle;
|
||||||
|
};
|
||||||
|
}
|
||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "application_wsi.hpp"
|
||||||
|
#include "application_wsi_events.hpp"
|
||||||
|
#include "application_events.hpp"
|
||||||
|
#include "global_managers.hpp"
|
||||||
|
#include "event.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
GraniteWSIPlatform::GraniteWSIPlatform()
|
||||||
|
{
|
||||||
|
input_tracker.set_input_handler(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::event_swapchain_created(Vulkan::Device *device, VkSwapchainKHR, unsigned width, unsigned height,
|
||||||
|
float aspect_ratio, size_t image_count, VkFormat format,
|
||||||
|
VkColorSpaceKHR color_space,
|
||||||
|
VkSurfaceTransformFlagBitsKHR transform)
|
||||||
|
{
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
{
|
||||||
|
em->enqueue_latched<Vulkan::SwapchainParameterEvent>(device, width, height, aspect_ratio, image_count, format,
|
||||||
|
color_space, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::event_swapchain_destroyed()
|
||||||
|
{
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
em->dequeue_all_latched(Vulkan::SwapchainParameterEvent::get_type_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::event_device_created(Vulkan::Device *device)
|
||||||
|
{
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
em->enqueue_latched<Vulkan::DeviceCreatedEvent>(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::event_device_destroyed()
|
||||||
|
{
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
em->dequeue_all_latched(Vulkan::DeviceCreatedEvent::get_type_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::event_swapchain_index(Vulkan::Device *device, unsigned index)
|
||||||
|
{
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
{
|
||||||
|
em->dequeue_all_latched(Vulkan::SwapchainIndexEvent::get_type_id());
|
||||||
|
em->enqueue_latched<Vulkan::SwapchainIndexEvent>(device, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::event_frame_tick(double, double)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void GraniteWSIPlatform::dispatch_template(const T &t)
|
||||||
|
{
|
||||||
|
if (auto *em = GRANITE_EVENT_MANAGER())
|
||||||
|
em->dispatch_inline(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void GraniteWSIPlatform::dispatch_template_filter(const T &t)
|
||||||
|
{
|
||||||
|
auto *ui = Global::ui_manager();
|
||||||
|
if (ui && !ui->filter_input_event(t))
|
||||||
|
return;
|
||||||
|
dispatch_template(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Func>
|
||||||
|
void GraniteWSIPlatform::dispatch_or_defer(Func &&func)
|
||||||
|
{
|
||||||
|
if (in_async_input)
|
||||||
|
captured.emplace_back(std::forward<Func>(func));
|
||||||
|
else
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
|
||||||
|
#define WORK(work) dispatch_or_defer([this, e]() { work; })
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const TouchDownEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const TouchUpEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const TouchGestureEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const JoypadButtonEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const JoypadAxisEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const KeyboardEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const OrientationEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const MouseButtonEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const MouseMoveEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template_filter(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const JoypadStateEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const InputStateEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::dispatch(const JoypadConnectionEvent &e)
|
||||||
|
{
|
||||||
|
WORK(dispatch_template(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::begin_async_input_handling()
|
||||||
|
{
|
||||||
|
in_async_input = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::end_async_input_handling()
|
||||||
|
{
|
||||||
|
in_async_input = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraniteWSIPlatform::flush_deferred_input_events()
|
||||||
|
{
|
||||||
|
VK_ASSERT(!in_async_input);
|
||||||
|
for (auto &func : captured)
|
||||||
|
func();
|
||||||
|
captured.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "wsi.hpp"
|
||||||
|
#include "input.hpp"
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class GraniteWSIPlatform : public Vulkan::WSIPlatform, private InputTrackerHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GraniteWSIPlatform();
|
||||||
|
|
||||||
|
InputTracker &get_input_tracker()
|
||||||
|
{
|
||||||
|
return input_tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void event_device_created(Vulkan::Device *device) override;
|
||||||
|
void event_device_destroyed() override;
|
||||||
|
void event_swapchain_created(Vulkan::Device *device, VkSwapchainKHR swapchain,
|
||||||
|
unsigned width, unsigned height,
|
||||||
|
float aspect_ratio, size_t image_count,
|
||||||
|
VkFormat format, VkColorSpaceKHR color_space,
|
||||||
|
VkSurfaceTransformFlagBitsKHR pre_rotate) override;
|
||||||
|
void event_swapchain_destroyed() override;
|
||||||
|
void event_swapchain_index(Vulkan::Device *device, unsigned index) override;
|
||||||
|
void event_frame_tick(double frame, double elapsed) override;
|
||||||
|
|
||||||
|
void begin_async_input_handling();
|
||||||
|
void end_async_input_handling();
|
||||||
|
void flush_deferred_input_events();
|
||||||
|
|
||||||
|
private:
|
||||||
|
InputTracker input_tracker;
|
||||||
|
void dispatch(const TouchDownEvent &e) override;
|
||||||
|
void dispatch(const TouchUpEvent &e) override;
|
||||||
|
void dispatch(const TouchGestureEvent &e) override;
|
||||||
|
void dispatch(const JoypadButtonEvent &e) override;
|
||||||
|
void dispatch(const JoypadAxisEvent &e) override;
|
||||||
|
void dispatch(const KeyboardEvent &e) override;
|
||||||
|
void dispatch(const OrientationEvent &e) override;
|
||||||
|
void dispatch(const MouseButtonEvent &e) override;
|
||||||
|
void dispatch(const MouseMoveEvent &e) override;
|
||||||
|
void dispatch(const JoypadStateEvent &e) override;
|
||||||
|
void dispatch(const InputStateEvent &e) override;
|
||||||
|
void dispatch(const JoypadConnectionEvent &e) override;
|
||||||
|
template <typename T>
|
||||||
|
void dispatch_template_filter(const T &t);
|
||||||
|
template <typename T>
|
||||||
|
void dispatch_template(const T &t);
|
||||||
|
template <typename Func>
|
||||||
|
void dispatch_or_defer(Func &&func);
|
||||||
|
|
||||||
|
bool in_async_input = false;
|
||||||
|
std::vector<std::function<void ()>> captured;
|
||||||
|
};
|
||||||
|
}
|
||||||
+258
@@ -0,0 +1,258 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "event.hpp"
|
||||||
|
#include "vulkan_headers.hpp"
|
||||||
|
|
||||||
|
namespace Vulkan
|
||||||
|
{
|
||||||
|
class Device;
|
||||||
|
class ShaderManager;
|
||||||
|
class WSIPlatform;
|
||||||
|
|
||||||
|
class DeviceCreatedEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(DeviceCreatedEvent)
|
||||||
|
|
||||||
|
explicit DeviceCreatedEvent(Device *device_)
|
||||||
|
: device(*device_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Device &get_device() const
|
||||||
|
{
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Device &device;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeviceShaderModuleReadyEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(DeviceShaderModuleReadyEvent)
|
||||||
|
|
||||||
|
explicit DeviceShaderModuleReadyEvent(Device *device_, ShaderManager *manager_)
|
||||||
|
: device(*device_), manager(*manager_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Device &get_device() const
|
||||||
|
{
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderManager &get_shader_manager() const
|
||||||
|
{
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Device &device;
|
||||||
|
ShaderManager &manager;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DevicePipelineReadyEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(DevicePipelineReadyEvent)
|
||||||
|
|
||||||
|
explicit DevicePipelineReadyEvent(Device *device_, ShaderManager *manager_)
|
||||||
|
: device(*device_), manager(*manager_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Device &get_device() const
|
||||||
|
{
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderManager &get_shader_manager() const
|
||||||
|
{
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Device &device;
|
||||||
|
ShaderManager &manager;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SwapchainParameterEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(SwapchainParameterEvent)
|
||||||
|
|
||||||
|
SwapchainParameterEvent(Device *device_,
|
||||||
|
unsigned width_, unsigned height_,
|
||||||
|
float aspect_ratio_, unsigned count_,
|
||||||
|
VkFormat format_, VkColorSpaceKHR color_space_,
|
||||||
|
VkSurfaceTransformFlagBitsKHR transform_)
|
||||||
|
: device(*device_), width(width_), height(height_),
|
||||||
|
aspect_ratio(aspect_ratio_), image_count(count_), format(format_), color_space(color_space_), transform(transform_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Device &get_device() const
|
||||||
|
{
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_width() const
|
||||||
|
{
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_height() const
|
||||||
|
{
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_aspect_ratio() const
|
||||||
|
{
|
||||||
|
return aspect_ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_image_count() const
|
||||||
|
{
|
||||||
|
return image_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkFormat get_format() const
|
||||||
|
{
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkColorSpaceKHR get_color_space() const
|
||||||
|
{
|
||||||
|
return color_space;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkSurfaceTransformFlagBitsKHR get_prerotate() const
|
||||||
|
{
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Device &device;
|
||||||
|
unsigned width;
|
||||||
|
unsigned height;
|
||||||
|
float aspect_ratio;
|
||||||
|
unsigned image_count;
|
||||||
|
VkFormat format;
|
||||||
|
VkColorSpaceKHR color_space;
|
||||||
|
VkSurfaceTransformFlagBitsKHR transform;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SwapchainIndexEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(SwapchainIndexEvent)
|
||||||
|
|
||||||
|
SwapchainIndexEvent(Device *device_, unsigned index_)
|
||||||
|
: device(*device_), index(index_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Device &get_device() const
|
||||||
|
{
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_index() const
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Device &device;
|
||||||
|
unsigned index;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApplicationWSIPlatformEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(ApplicationWSIPlatformEvent)
|
||||||
|
|
||||||
|
explicit ApplicationWSIPlatformEvent(WSIPlatform &platform_)
|
||||||
|
: platform(platform_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
WSIPlatform &get_platform() const
|
||||||
|
{
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
WSIPlatform &platform;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApplicationWindowFileDropEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(ApplicationWindowFileDropEvent)
|
||||||
|
explicit ApplicationWindowFileDropEvent(std::string path_)
|
||||||
|
: path(std::move(path_))
|
||||||
|
{}
|
||||||
|
|
||||||
|
const std::string &get_path() const
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApplicationWindowTextDropEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(ApplicationWindowTextDropEvent)
|
||||||
|
explicit ApplicationWindowTextDropEvent(std::string str_)
|
||||||
|
: str(std::move(str_))
|
||||||
|
{}
|
||||||
|
|
||||||
|
const std::string &get_text() const
|
||||||
|
{
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string str;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApplicationSoftKeyboardUpdateEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(ApplicationSoftKeyboardUpdateEvent)
|
||||||
|
explicit ApplicationSoftKeyboardUpdateEvent(std::string str_)
|
||||||
|
: str(std::move(str_))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string &get_text() const
|
||||||
|
{
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string str;
|
||||||
|
};
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
add_granite_internal_lib(granite-application-global
|
||||||
|
global_managers.hpp global_managers.cpp)
|
||||||
|
target_include_directories(granite-application-global PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(granite-application-global PUBLIC granite-util)
|
||||||
|
|
||||||
|
add_granite_internal_lib(granite-application-global-init
|
||||||
|
global_managers_init.hpp global_managers_init.cpp)
|
||||||
|
target_include_directories(granite-application-global-init PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(granite-application-global-init
|
||||||
|
PUBLIC granite-application-global
|
||||||
|
PRIVATE granite-threading granite-event granite-filesystem)
|
||||||
|
|
||||||
|
if (TARGET granite-renderer)
|
||||||
|
target_link_libraries(granite-application-global-init PRIVATE granite-renderer granite-ui)
|
||||||
|
target_compile_definitions(granite-application-global-init PRIVATE HAVE_GRANITE_RENDERER)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (GRANITE_AUDIO)
|
||||||
|
target_link_libraries(granite-application-global-init PRIVATE granite-audio)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (GRANITE_BULLET)
|
||||||
|
target_link_libraries(granite-application-global-init PRIVATE granite-physics)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(granite-application-global-interface INTERFACE)
|
||||||
|
target_include_directories(granite-application-global-interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
+308
@@ -0,0 +1,308 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "global_managers.hpp"
|
||||||
|
#include "environment.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include <thread>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
namespace Global
|
||||||
|
{
|
||||||
|
// Could use unique_ptr here, but would be nice to avoid global ctor/dtor.
|
||||||
|
struct GlobalManagers
|
||||||
|
{
|
||||||
|
Factory *factory;
|
||||||
|
|
||||||
|
FilesystemInterface *filesystem;
|
||||||
|
AssetManagerInterface *asset_manager;
|
||||||
|
MaterialManagerInterface *material_manager;
|
||||||
|
EventManagerInterface *event_manager;
|
||||||
|
ThreadGroupInterface *thread_group;
|
||||||
|
UI::UIManagerInterface *ui_manager;
|
||||||
|
CommonRendererDataInterface *common_renderer_data;
|
||||||
|
Util::MessageQueueInterface *logging;
|
||||||
|
Audio::BackendInterface *audio_backend;
|
||||||
|
Audio::MixerInterface *audio_mixer;
|
||||||
|
PhysicsSystemInterface *physics;
|
||||||
|
};
|
||||||
|
|
||||||
|
static thread_local GlobalManagers global_managers;
|
||||||
|
|
||||||
|
GlobalManagersHandle create_thread_context()
|
||||||
|
{
|
||||||
|
return GlobalManagersHandle(new GlobalManagers(global_managers));
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete_thread_context(GlobalManagers *managers)
|
||||||
|
{
|
||||||
|
delete managers;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GlobalManagerDeleter::operator()(GlobalManagers *managers)
|
||||||
|
{
|
||||||
|
delete_thread_context(managers);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_thread_context(const GlobalManagers &managers)
|
||||||
|
{
|
||||||
|
global_managers = managers;
|
||||||
|
if (managers.thread_group)
|
||||||
|
managers.thread_group->set_thread_context();
|
||||||
|
if (managers.logging)
|
||||||
|
Util::set_thread_logging_interface(managers.logging);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_thread_context()
|
||||||
|
{
|
||||||
|
global_managers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Util::MessageQueueInterface *message_queue()
|
||||||
|
{
|
||||||
|
return global_managers.logging;
|
||||||
|
}
|
||||||
|
|
||||||
|
FilesystemInterface *filesystem()
|
||||||
|
{
|
||||||
|
return global_managers.filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetManagerInterface *asset_manager()
|
||||||
|
{
|
||||||
|
return global_managers.asset_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialManagerInterface *material_manager()
|
||||||
|
{
|
||||||
|
return global_managers.material_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventManagerInterface *event_manager()
|
||||||
|
{
|
||||||
|
return global_managers.event_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadGroupInterface *thread_group()
|
||||||
|
{
|
||||||
|
return global_managers.thread_group;
|
||||||
|
}
|
||||||
|
|
||||||
|
UI::UIManagerInterface *ui_manager()
|
||||||
|
{
|
||||||
|
return global_managers.ui_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonRendererDataInterface *common_renderer_data()
|
||||||
|
{
|
||||||
|
return global_managers.common_renderer_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Audio::BackendInterface *audio_backend() { return global_managers.audio_backend; }
|
||||||
|
Audio::MixerInterface *audio_mixer() { return global_managers.audio_mixer; }
|
||||||
|
|
||||||
|
void install_audio_system(Audio::BackendInterface *backend, Audio::MixerInterface *mixer)
|
||||||
|
{
|
||||||
|
delete global_managers.audio_mixer;
|
||||||
|
global_managers.audio_mixer = mixer;
|
||||||
|
delete global_managers.audio_backend;
|
||||||
|
global_managers.audio_backend = backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
PhysicsSystemInterface *physics()
|
||||||
|
{
|
||||||
|
return global_managers.physics;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(Factory &factory, ManagerFeatureFlags flags, unsigned max_threads, float audio_sample_rate)
|
||||||
|
{
|
||||||
|
assert(!global_managers.factory || global_managers.factory == &factory);
|
||||||
|
global_managers.factory = &factory;
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_EVENT_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.event_manager)
|
||||||
|
global_managers.event_manager = factory.create_event_manager();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_FILESYSTEM_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.filesystem)
|
||||||
|
global_managers.filesystem = factory.create_filesystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_ASSET_MANAGER_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.asset_manager)
|
||||||
|
global_managers.asset_manager = factory.create_asset_manager();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_MATERIAL_MANAGER_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.material_manager)
|
||||||
|
global_managers.material_manager = factory.create_material_manager();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool kick_threads = false;
|
||||||
|
if (flags & MANAGER_FEATURE_THREAD_GROUP_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.thread_group)
|
||||||
|
{
|
||||||
|
global_managers.thread_group = factory.create_thread_group();
|
||||||
|
kick_threads = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_UI_MANAGER_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.ui_manager)
|
||||||
|
global_managers.ui_manager = factory.create_ui_manager();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.common_renderer_data)
|
||||||
|
global_managers.common_renderer_data = factory.create_common_renderer_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_LOGGING_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.logging)
|
||||||
|
global_managers.logging = factory.create_message_queue();
|
||||||
|
Util::set_thread_logging_interface(global_managers.logging);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_PHYSICS_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.physics)
|
||||||
|
global_managers.physics = factory.create_physics_system();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_AUDIO_MIXER_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.audio_mixer)
|
||||||
|
global_managers.audio_mixer = factory.create_audio_mixer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & MANAGER_FEATURE_AUDIO_BACKEND_BIT)
|
||||||
|
{
|
||||||
|
if (!global_managers.audio_backend)
|
||||||
|
global_managers.audio_backend = factory.create_audio_backend(global_managers.audio_mixer, audio_sample_rate, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick threads after all global managers are set up.
|
||||||
|
if (kick_threads)
|
||||||
|
{
|
||||||
|
unsigned cpu_threads = std::thread::hardware_concurrency();
|
||||||
|
cpu_threads = cpu_threads > 1 ? (cpu_threads - 1u) : 1u;
|
||||||
|
|
||||||
|
if (cpu_threads > max_threads)
|
||||||
|
cpu_threads = max_threads;
|
||||||
|
cpu_threads = Util::get_environment_uint("GRANITE_NUM_WORKER_THREADS", cpu_threads);
|
||||||
|
|
||||||
|
unsigned background_cpu_threads = (cpu_threads + 1) / 2;
|
||||||
|
|
||||||
|
global_managers.thread_group->start(cpu_threads, background_cpu_threads,
|
||||||
|
[ctx = std::shared_ptr<GlobalManagers>(create_thread_context())] {
|
||||||
|
set_thread_context(*ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void deinit()
|
||||||
|
{
|
||||||
|
if (!global_managers.factory)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (global_managers.audio_backend)
|
||||||
|
global_managers.audio_backend->stop();
|
||||||
|
|
||||||
|
delete global_managers.audio_backend;
|
||||||
|
delete global_managers.audio_mixer;
|
||||||
|
delete global_managers.physics;
|
||||||
|
delete global_managers.common_renderer_data;
|
||||||
|
delete global_managers.ui_manager;
|
||||||
|
delete global_managers.thread_group;
|
||||||
|
delete global_managers.material_manager;
|
||||||
|
delete global_managers.asset_manager;
|
||||||
|
delete global_managers.filesystem;
|
||||||
|
delete global_managers.event_manager;
|
||||||
|
delete global_managers.logging;
|
||||||
|
|
||||||
|
global_managers.audio_backend = nullptr;
|
||||||
|
global_managers.audio_mixer = nullptr;
|
||||||
|
global_managers.physics = nullptr;
|
||||||
|
global_managers.common_renderer_data = nullptr;
|
||||||
|
global_managers.filesystem = nullptr;
|
||||||
|
global_managers.material_manager = nullptr;
|
||||||
|
global_managers.asset_manager = nullptr;
|
||||||
|
global_managers.event_manager = nullptr;
|
||||||
|
global_managers.thread_group = nullptr;
|
||||||
|
global_managers.ui_manager = nullptr;
|
||||||
|
global_managers.logging = nullptr;
|
||||||
|
|
||||||
|
global_managers.factory = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_audio_system()
|
||||||
|
{
|
||||||
|
if (!global_managers.audio_backend)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!global_managers.audio_backend->start())
|
||||||
|
{
|
||||||
|
LOGE("Failed to start audio subsystem!\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global_managers.event_manager && global_managers.audio_mixer)
|
||||||
|
global_managers.audio_mixer->event_start(*global_managers.event_manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop_audio_system()
|
||||||
|
{
|
||||||
|
if (!global_managers.audio_backend)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!global_managers.audio_backend->stop())
|
||||||
|
LOGE("Failed to stop audio subsystem!\n");
|
||||||
|
|
||||||
|
if (global_managers.event_manager && global_managers.audio_mixer)
|
||||||
|
global_managers.audio_mixer->event_stop(*global_managers.event_manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
FilesystemInterface *Factory::create_filesystem() { return nullptr; }
|
||||||
|
AssetManagerInterface *Factory::create_asset_manager() { return nullptr; }
|
||||||
|
MaterialManagerInterface *Factory::create_material_manager() { return nullptr; }
|
||||||
|
EventManagerInterface *Factory::create_event_manager() { return nullptr; }
|
||||||
|
ThreadGroupInterface *Factory::create_thread_group() { return nullptr; }
|
||||||
|
CommonRendererDataInterface *Factory::create_common_renderer_data() { return nullptr; }
|
||||||
|
PhysicsSystemInterface *Factory::create_physics_system() { return nullptr; }
|
||||||
|
Audio::BackendInterface *Factory::create_audio_backend(Audio::MixerInterface *, float, unsigned) { return nullptr; }
|
||||||
|
Audio::MixerInterface *Factory::create_audio_mixer() { return nullptr; }
|
||||||
|
UI::UIManagerInterface *Factory::create_ui_manager() { return nullptr; }
|
||||||
|
Util::MessageQueueInterface *Factory::create_message_queue() { return nullptr; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <limits.h>
|
||||||
|
#include "global_managers_interface.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
namespace Global
|
||||||
|
{
|
||||||
|
enum ManagerFeatureFlagBits
|
||||||
|
{
|
||||||
|
MANAGER_FEATURE_FILESYSTEM_BIT = 1 << 0,
|
||||||
|
MANAGER_FEATURE_EVENT_BIT = 1 << 1,
|
||||||
|
MANAGER_FEATURE_THREAD_GROUP_BIT = 1 << 2,
|
||||||
|
MANAGER_FEATURE_UI_MANAGER_BIT = 1 << 3,
|
||||||
|
MANAGER_FEATURE_AUDIO_MIXER_BIT = 1 << 4,
|
||||||
|
MANAGER_FEATURE_AUDIO_BACKEND_BIT = 1 << 5,
|
||||||
|
MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT = 1 << 6,
|
||||||
|
MANAGER_FEATURE_PHYSICS_BIT = 1 << 7,
|
||||||
|
MANAGER_FEATURE_LOGGING_BIT = 1 << 8,
|
||||||
|
MANAGER_FEATURE_ASSET_MANAGER_BIT = 1 << 9,
|
||||||
|
MANAGER_FEATURE_MATERIAL_MANAGER_BIT = 1 << 10,
|
||||||
|
MANAGER_FEATURE_DEFAULT_BITS = (MANAGER_FEATURE_FILESYSTEM_BIT |
|
||||||
|
MANAGER_FEATURE_ASSET_MANAGER_BIT |
|
||||||
|
MANAGER_FEATURE_MATERIAL_MANAGER_BIT |
|
||||||
|
MANAGER_FEATURE_EVENT_BIT |
|
||||||
|
MANAGER_FEATURE_THREAD_GROUP_BIT |
|
||||||
|
MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT |
|
||||||
|
MANAGER_FEATURE_UI_MANAGER_BIT |
|
||||||
|
MANAGER_FEATURE_AUDIO_MIXER_BIT |
|
||||||
|
MANAGER_FEATURE_AUDIO_BACKEND_BIT)
|
||||||
|
};
|
||||||
|
using ManagerFeatureFlags = uint32_t;
|
||||||
|
|
||||||
|
// Decouple creation from global TLS storage.
|
||||||
|
// This avoids some nasty cyclical dependencies.
|
||||||
|
class Factory
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~Factory() = default;
|
||||||
|
|
||||||
|
virtual FilesystemInterface *create_filesystem();
|
||||||
|
virtual AssetManagerInterface *create_asset_manager();
|
||||||
|
virtual MaterialManagerInterface *create_material_manager();
|
||||||
|
virtual EventManagerInterface *create_event_manager();
|
||||||
|
virtual ThreadGroupInterface *create_thread_group();
|
||||||
|
virtual CommonRendererDataInterface *create_common_renderer_data();
|
||||||
|
virtual PhysicsSystemInterface *create_physics_system();
|
||||||
|
virtual Audio::BackendInterface *create_audio_backend(Audio::MixerInterface *mixer,
|
||||||
|
float sample_rate,
|
||||||
|
unsigned channels);
|
||||||
|
virtual Audio::MixerInterface *create_audio_mixer();
|
||||||
|
virtual UI::UIManagerInterface *create_ui_manager();
|
||||||
|
virtual Util::MessageQueueInterface *create_message_queue();
|
||||||
|
};
|
||||||
|
|
||||||
|
void init(Factory &factory, ManagerFeatureFlags flags = MANAGER_FEATURE_DEFAULT_BITS,
|
||||||
|
unsigned max_threads = UINT_MAX, float audio_sample_rate = -1.0f);
|
||||||
|
void deinit();
|
||||||
|
|
||||||
|
// Used if the application wants to use multiple instances of Granite in the same process.
|
||||||
|
// This allows each thread to be associated to a global context.
|
||||||
|
struct GlobalManagers;
|
||||||
|
struct GlobalManagerDeleter
|
||||||
|
{
|
||||||
|
void operator()(GlobalManagers *managers);
|
||||||
|
};
|
||||||
|
using GlobalManagersHandle = std::unique_ptr<GlobalManagers, GlobalManagerDeleter>;
|
||||||
|
GlobalManagersHandle create_thread_context();
|
||||||
|
void delete_thread_context(GlobalManagers *managers);
|
||||||
|
void set_thread_context(const GlobalManagers &managers);
|
||||||
|
void clear_thread_context();
|
||||||
|
|
||||||
|
void start_audio_system();
|
||||||
|
void stop_audio_system();
|
||||||
|
void install_audio_system(Audio::BackendInterface *backend, Audio::MixerInterface *mixer);
|
||||||
|
|
||||||
|
Util::MessageQueueInterface *message_queue();
|
||||||
|
FilesystemInterface *filesystem();
|
||||||
|
AssetManagerInterface *asset_manager();
|
||||||
|
MaterialManagerInterface *material_manager();
|
||||||
|
EventManagerInterface *event_manager();
|
||||||
|
ThreadGroupInterface *thread_group();
|
||||||
|
UI::UIManagerInterface *ui_manager();
|
||||||
|
CommonRendererDataInterface *common_renderer_data();
|
||||||
|
Audio::BackendInterface *audio_backend();
|
||||||
|
Audio::MixerInterface *audio_mixer();
|
||||||
|
PhysicsSystemInterface *physics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#define GRANITE_MESSAGE_QUEUE() static_cast<::Util::MessageQueue *>(::Granite::Global::message_queue())
|
||||||
|
#define GRANITE_FILESYSTEM() static_cast<::Granite::Filesystem *>(::Granite::Global::filesystem())
|
||||||
|
#define GRANITE_ASSET_MANAGER() static_cast<::Granite::AssetManager *>(::Granite::Global::asset_manager())
|
||||||
|
#define GRANITE_MATERIAL_MANAGER() static_cast<::Granite::MaterialManager *>(::Granite::Global::material_manager())
|
||||||
|
#define GRANITE_EVENT_MANAGER() static_cast<::Granite::EventManager *>(::Granite::Global::event_manager())
|
||||||
|
#define GRANITE_THREAD_GROUP() static_cast<::Granite::ThreadGroup *>(::Granite::Global::thread_group())
|
||||||
|
#define GRANITE_UI_MANAGER() static_cast<::Granite::UI::UIManager *>(::Granite::Global::ui_manager())
|
||||||
|
#define GRANITE_COMMON_RENDERER_DATA() static_cast<::Granite::CommonRendererData *>(::Granite::Global::common_renderer_data())
|
||||||
|
#define GRANITE_AUDIO_BACKEND() static_cast<::Granite::Audio::Backend *>(::Granite::Global::audio_backend())
|
||||||
|
#define GRANITE_AUDIO_MIXER() static_cast<::Granite::Audio::Mixer *>(::Granite::Global::audio_mixer())
|
||||||
|
#define GRANITE_PHYSICS() static_cast<::Granite::PhysicsSystem *>(::Granite::Global::physics())
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "global_managers_init.hpp"
|
||||||
|
#include "global_managers.hpp"
|
||||||
|
#include "event.hpp"
|
||||||
|
#include "thread_group.hpp"
|
||||||
|
#include "filesystem.hpp"
|
||||||
|
#include "asset_manager.hpp"
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
#include "material_manager.hpp"
|
||||||
|
#include "common_renderer_data.hpp"
|
||||||
|
#include "ui_manager.hpp"
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
#include "audio_mixer.hpp"
|
||||||
|
#include "audio_interface.hpp"
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_GRANITE_PHYSICS
|
||||||
|
#include "physics_system.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
namespace Global
|
||||||
|
{
|
||||||
|
struct FactoryImplementation : Factory
|
||||||
|
{
|
||||||
|
FilesystemInterface *create_filesystem() override
|
||||||
|
{
|
||||||
|
return new Filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetManagerInterface *create_asset_manager() override
|
||||||
|
{
|
||||||
|
return new AssetManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventManagerInterface *create_event_manager() override
|
||||||
|
{
|
||||||
|
return new EventManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadGroupInterface *create_thread_group() override
|
||||||
|
{
|
||||||
|
return new ThreadGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonRendererDataInterface *create_common_renderer_data() override
|
||||||
|
{
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
return new CommonRendererData;
|
||||||
|
#else
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
UI::UIManagerInterface *create_ui_manager() override
|
||||||
|
{
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
return new UI::UIManager;
|
||||||
|
#else
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialManagerInterface *create_material_manager() override
|
||||||
|
{
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
return new MaterialManager;
|
||||||
|
#else
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Audio::MixerInterface *create_audio_mixer() override
|
||||||
|
{
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
return new Audio::Mixer;
|
||||||
|
#else
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Audio::BackendInterface *create_audio_backend(Audio::MixerInterface *iface, float sample_rate, unsigned channels) override
|
||||||
|
{
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
if (iface)
|
||||||
|
return Audio::create_default_audio_backend(static_cast<Audio::Mixer *>(iface), sample_rate, channels);
|
||||||
|
else
|
||||||
|
return nullptr;
|
||||||
|
#else
|
||||||
|
(void)iface;
|
||||||
|
(void)sample_rate;
|
||||||
|
(void)channels;
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
PhysicsSystemInterface *create_physics_system() override
|
||||||
|
{
|
||||||
|
#ifdef HAVE_GRANITE_PHYSICS
|
||||||
|
return new PhysicsSystem;
|
||||||
|
#else
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static FactoryImplementation factory;
|
||||||
|
|
||||||
|
void init(ManagerFeatureFlags flags, unsigned max_threads, float audio_sample_rate)
|
||||||
|
{
|
||||||
|
init(factory, flags, max_threads, audio_sample_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "global_managers.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
namespace Global
|
||||||
|
{
|
||||||
|
void init(ManagerFeatureFlags flags = MANAGER_FEATURE_DEFAULT_BITS,
|
||||||
|
unsigned max_threads = UINT_MAX, float audio_sample_rate = -1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+133
@@ -0,0 +1,133 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include "logging.hpp"
|
||||||
|
|
||||||
|
namespace Util
|
||||||
|
{
|
||||||
|
class MessageQueueInterface : public LoggingInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~MessageQueueInterface() = default;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class FilesystemInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~FilesystemInterface() = default;
|
||||||
|
virtual bool load_text_file(const std::string &path, std::string &str) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AssetManagerInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~AssetManagerInterface() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MaterialManagerInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~MaterialManagerInterface() = default;
|
||||||
|
virtual void iterate(AssetManagerInterface *iface) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThreadGroupInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~ThreadGroupInterface() = default;
|
||||||
|
virtual void start(unsigned foreground_count, unsigned background_count,
|
||||||
|
const std::function<void()> &cb) = 0;
|
||||||
|
virtual void set_thread_context() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EventManagerInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~EventManagerInterface() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CommonRendererDataInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~CommonRendererDataInterface() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PhysicsSystemInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~PhysicsSystemInterface() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TouchDownEvent;
|
||||||
|
class TouchUpEvent;
|
||||||
|
class MouseMoveEvent;
|
||||||
|
class KeyboardEvent;
|
||||||
|
class OrientationEvent;
|
||||||
|
class TouchGestureEvent;
|
||||||
|
class MouseButtonEvent;
|
||||||
|
class JoypadButtonEvent;
|
||||||
|
class JoypadAxisEvent;
|
||||||
|
|
||||||
|
namespace UI
|
||||||
|
{
|
||||||
|
class UIManagerInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~UIManagerInterface() = default;
|
||||||
|
virtual bool filter_input_event(const TouchDownEvent &e) = 0;
|
||||||
|
virtual bool filter_input_event(const TouchUpEvent &e) = 0;
|
||||||
|
virtual bool filter_input_event(const MouseMoveEvent &e) = 0;
|
||||||
|
virtual bool filter_input_event(const KeyboardEvent &e) = 0;
|
||||||
|
virtual bool filter_input_event(const OrientationEvent &e) = 0;
|
||||||
|
virtual bool filter_input_event(const TouchGestureEvent &e) = 0;
|
||||||
|
virtual bool filter_input_event(const MouseButtonEvent &e) = 0;
|
||||||
|
virtual bool filter_input_event(const JoypadButtonEvent &e) = 0;
|
||||||
|
virtual bool filter_input_event(const JoypadAxisEvent &e) = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Audio
|
||||||
|
{
|
||||||
|
class BackendInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~BackendInterface() = default;
|
||||||
|
virtual bool start() = 0;
|
||||||
|
virtual bool stop() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MixerInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~MixerInterface() = default;
|
||||||
|
virtual void event_start(EventManagerInterface &event_manager) = 0;
|
||||||
|
virtual void event_stop(EventManagerInterface &event_manager) = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
add_granite_internal_lib(granite-input input.hpp input.cpp)
|
||||||
|
target_include_directories(granite-input PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(granite-input PUBLIC granite-util granite-event granite-math)
|
||||||
|
|
||||||
|
if (${GRANITE_PLATFORM} MATCHES "SDL")
|
||||||
|
add_granite_internal_static_lib(granite-input-sdl input_sdl.cpp input_sdl.hpp)
|
||||||
|
target_link_libraries(granite-input-sdl PUBLIC granite-input)
|
||||||
|
if (GRANITE_SYSTEM_SDL)
|
||||||
|
find_package(SDL3 REQUIRED)
|
||||||
|
target_link_libraries(granite-input-sdl PUBLIC SDL3::SDL3-shared)
|
||||||
|
else()
|
||||||
|
target_link_libraries(granite-input-sdl PUBLIC SDL3-static)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "input.hpp"
|
||||||
|
#include "event.hpp"
|
||||||
|
#include "muglm/muglm_impl.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
using namespace Util;
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
const char *joypad_key_to_tag(JoypadKey key)
|
||||||
|
{
|
||||||
|
#define D(k) case JoypadKey::k: return #k
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
D(Left);
|
||||||
|
D(Right);
|
||||||
|
D(Up);
|
||||||
|
D(Down);
|
||||||
|
D(LeftShoulder);
|
||||||
|
D(RightShoulder);
|
||||||
|
D(West);
|
||||||
|
D(East);
|
||||||
|
D(North);
|
||||||
|
D(South);
|
||||||
|
D(LeftThumb);
|
||||||
|
D(RightThumb);
|
||||||
|
D(Mode);
|
||||||
|
D(Start);
|
||||||
|
D(Select);
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
#undef D
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *joypad_axis_to_tag(JoypadAxis axis)
|
||||||
|
{
|
||||||
|
#define D(k) case JoypadAxis::k: return #k
|
||||||
|
switch (axis)
|
||||||
|
{
|
||||||
|
D(LeftX);
|
||||||
|
D(LeftY);
|
||||||
|
D(RightX);
|
||||||
|
D(RightY);
|
||||||
|
D(LeftTrigger);
|
||||||
|
D(RightTrigger);
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
#undef D
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::orientation_event(quat rot)
|
||||||
|
{
|
||||||
|
OrientationEvent event(rot);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::on_touch_down(unsigned id, float x, float y)
|
||||||
|
{
|
||||||
|
if (touch.active_pointers >= TouchCount)
|
||||||
|
{
|
||||||
|
LOGE("Touch pointer overflow!\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned index = touch.active_pointers++;
|
||||||
|
auto &pointer = touch.pointers[index];
|
||||||
|
pointer.id = id;
|
||||||
|
pointer.start_x = x;
|
||||||
|
pointer.start_y = y;
|
||||||
|
pointer.last_x = x;
|
||||||
|
pointer.last_y = y;
|
||||||
|
pointer.x = x;
|
||||||
|
pointer.y = y;
|
||||||
|
|
||||||
|
TouchDownEvent event(index, id, x, y, touch.width, touch.height);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::dispatch_touch_gesture()
|
||||||
|
{
|
||||||
|
TouchGestureEvent event(touch);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::on_touch_move(unsigned id, float x, float y)
|
||||||
|
{
|
||||||
|
auto &pointers = touch.pointers;
|
||||||
|
auto itr = std::find_if(std::begin(pointers), std::begin(pointers) + touch.active_pointers, [id](const TouchState::Pointer &pointer) {
|
||||||
|
return pointer.id == id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itr == std::end(pointers))
|
||||||
|
{
|
||||||
|
LOGE("Could not find pointer!\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itr->x = x;
|
||||||
|
itr->y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::on_touch_up(unsigned id, float x, float y)
|
||||||
|
{
|
||||||
|
auto &pointers = touch.pointers;
|
||||||
|
auto itr = std::find_if(std::begin(pointers), std::begin(pointers) + touch.active_pointers, [id](const TouchState::Pointer &pointer) {
|
||||||
|
return pointer.id == id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itr == std::end(pointers))
|
||||||
|
{
|
||||||
|
LOGE("Could not find pointer!\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto index = itr - std::begin(pointers);
|
||||||
|
|
||||||
|
TouchUpEvent event(itr->id, x, y, itr->start_x, itr->start_y, touch.width, touch.height);
|
||||||
|
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
|
||||||
|
memmove(&pointers[index], &pointers[index + 1], (TouchCount - (index + 1)) * sizeof(TouchState::Pointer));
|
||||||
|
touch.active_pointers--;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::joypad_key_state(unsigned index, JoypadKey key, JoypadKeyState state)
|
||||||
|
{
|
||||||
|
if (index >= Joypads)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert(active_joypads & (1u << index));
|
||||||
|
|
||||||
|
auto &joy = joypads[index];
|
||||||
|
unsigned key_index = Util::ecast(key);
|
||||||
|
unsigned key_mask = 1u << key_index;
|
||||||
|
if (state == JoypadKeyState::Pressed)
|
||||||
|
{
|
||||||
|
if ((joy.button_mask & key_mask) == 0)
|
||||||
|
{
|
||||||
|
JoypadButtonEvent event(index, key, state);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
joy.button_mask |= key_mask;
|
||||||
|
}
|
||||||
|
else if (state == JoypadKeyState::Released)
|
||||||
|
{
|
||||||
|
if ((joy.button_mask & key_mask) != 0)
|
||||||
|
{
|
||||||
|
JoypadButtonEvent event(index, key, state);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
joy.button_mask &= ~key_mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void JoypadState::snap_deadzone(float deadzone)
|
||||||
|
{
|
||||||
|
memcpy(snapped_axis, raw_axis, sizeof(raw_axis));
|
||||||
|
|
||||||
|
static const JoypadAxis fused_axes[2][2] = {
|
||||||
|
{ JoypadAxis::LeftX, JoypadAxis::LeftY },
|
||||||
|
{ JoypadAxis::RightX, JoypadAxis::RightY },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto &fused : fused_axes)
|
||||||
|
{
|
||||||
|
if (std::abs(raw_axis[int(fused[0])]) < deadzone && std::abs(raw_axis[int(fused[1])]) < deadzone)
|
||||||
|
for (auto &axis : fused)
|
||||||
|
snapped_axis[int(axis)] = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::joyaxis_state(unsigned index, JoypadAxis axis, float value)
|
||||||
|
{
|
||||||
|
if (index >= Joypads)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert(active_joypads & (1u << index));
|
||||||
|
|
||||||
|
auto &joy = joypads[index];
|
||||||
|
unsigned axis_index = Util::ecast(axis);
|
||||||
|
auto &a = joy.raw_axis[axis_index];
|
||||||
|
if (a != value)
|
||||||
|
{
|
||||||
|
JoypadAxisEvent event(index, axis, value);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
a = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::key_event(Key key, KeyState state)
|
||||||
|
{
|
||||||
|
if (state == KeyState::Released)
|
||||||
|
key_state &= ~(1ull << ecast(key));
|
||||||
|
else if (state == KeyState::Pressed)
|
||||||
|
key_state |= 1ull << ecast(key);
|
||||||
|
|
||||||
|
KeyboardEvent event(key, state);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::mouse_button_event(Granite::MouseButton button, bool pressed)
|
||||||
|
{
|
||||||
|
mouse_button_event(button, last_mouse_x, last_mouse_y, pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::mouse_button_event(MouseButton button, double x, double y, bool pressed)
|
||||||
|
{
|
||||||
|
if (pressed)
|
||||||
|
mouse_button_state |= 1ull << ecast(button);
|
||||||
|
else
|
||||||
|
mouse_button_state &= ~(1ull << ecast(button));
|
||||||
|
|
||||||
|
if (mouse_active)
|
||||||
|
{
|
||||||
|
last_mouse_x = x;
|
||||||
|
last_mouse_y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseButtonEvent event(button, x, y, pressed);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::mouse_move_event_relative(double x, double y)
|
||||||
|
{
|
||||||
|
x *= mouse_speed_x;
|
||||||
|
y *= mouse_speed_y;
|
||||||
|
if (mouse_active)
|
||||||
|
{
|
||||||
|
last_mouse_x += x;
|
||||||
|
last_mouse_y += y;
|
||||||
|
last_mouse_x = clamp(last_mouse_x, mouse_relative_range_x,
|
||||||
|
mouse_relative_range_x + mouse_relative_range_width);
|
||||||
|
last_mouse_y = clamp(last_mouse_y, mouse_relative_range_y,
|
||||||
|
mouse_relative_range_y + mouse_relative_range_height);
|
||||||
|
MouseMoveEvent event(x, y, last_mouse_x, last_mouse_y, key_state, mouse_button_state);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::mouse_move_event_absolute(double x, double y)
|
||||||
|
{
|
||||||
|
if (mouse_active)
|
||||||
|
{
|
||||||
|
double delta_x = x - last_mouse_x;
|
||||||
|
double delta_y = y - last_mouse_y;
|
||||||
|
last_mouse_x = x;
|
||||||
|
last_mouse_y = y;
|
||||||
|
MouseMoveEvent event(delta_x, delta_y, x, y, key_state, mouse_button_state);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::mouse_move_event_absolute_normalized(double x, double y)
|
||||||
|
{
|
||||||
|
mouse_move_event_absolute(x * double(touch.width), y * double(touch.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::mouse_button_event_normalized(MouseButton button, double x, double y, bool pressed)
|
||||||
|
{
|
||||||
|
mouse_button_event(button, x * double(touch.width), y * double(touch.height), pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::mouse_enter(double x, double y)
|
||||||
|
{
|
||||||
|
mouse_active = true;
|
||||||
|
last_mouse_x = x;
|
||||||
|
last_mouse_y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::mouse_leave()
|
||||||
|
{
|
||||||
|
mouse_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::dispatch_current_state(double delta_time, InputTrackerHandler *override_handler)
|
||||||
|
{
|
||||||
|
if (!override_handler)
|
||||||
|
override_handler = handler;
|
||||||
|
|
||||||
|
if (override_handler)
|
||||||
|
{
|
||||||
|
for (auto &pad : joypads)
|
||||||
|
pad.snap_deadzone(axis_deadzone);
|
||||||
|
|
||||||
|
override_handler->dispatch(JoypadStateEvent{active_joypads, joypads, Joypads, delta_time});
|
||||||
|
override_handler->dispatch(InputStateEvent{last_mouse_x, last_mouse_y,
|
||||||
|
delta_time, key_state, mouse_button_state, mouse_active});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int InputTracker::find_vacant_joypad_index() const
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Joypads; i++)
|
||||||
|
{
|
||||||
|
if ((active_joypads & (1 << i)) == 0)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::enable_joypad(unsigned index, uint32_t vid, uint32_t pid)
|
||||||
|
{
|
||||||
|
if (index >= Joypads)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (active_joypads & (1u << index))
|
||||||
|
return;
|
||||||
|
|
||||||
|
active_joypads |= 1u << index;
|
||||||
|
joypads[index] = {};
|
||||||
|
joypads[index].vid = vid;
|
||||||
|
joypads[index].pid = pid;
|
||||||
|
JoypadConnectionEvent event(index, true, vid, pid);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTracker::disable_joypad(unsigned index, uint32_t vid, uint32_t pid)
|
||||||
|
{
|
||||||
|
if (index >= Joypads)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ((active_joypads & (1u << index)) == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
active_joypads &= ~(1u << index);
|
||||||
|
joypads[index] = {};
|
||||||
|
JoypadConnectionEvent event(index, false, vid, pid);
|
||||||
|
if (handler)
|
||||||
|
handler->dispatch(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mutex &InputTracker::get_lock()
|
||||||
|
{
|
||||||
|
return dispatch_lock;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,719 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "enum_cast.hpp"
|
||||||
|
#include "event.hpp"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "math.hpp"
|
||||||
|
#include <limits.h>
|
||||||
|
#include <float.h>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
enum class JoypadKey
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
East,
|
||||||
|
South,
|
||||||
|
West,
|
||||||
|
North,
|
||||||
|
LeftShoulder,
|
||||||
|
RightShoulder,
|
||||||
|
LeftThumb,
|
||||||
|
RightThumb,
|
||||||
|
Start,
|
||||||
|
Select,
|
||||||
|
Mode,
|
||||||
|
Count,
|
||||||
|
Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
const char *joypad_key_to_tag(JoypadKey key);
|
||||||
|
|
||||||
|
enum class JoypadAxis
|
||||||
|
{
|
||||||
|
LeftX,
|
||||||
|
LeftY,
|
||||||
|
RightX,
|
||||||
|
RightY,
|
||||||
|
LeftTrigger,
|
||||||
|
RightTrigger,
|
||||||
|
Count,
|
||||||
|
Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
const char *joypad_axis_to_tag(JoypadAxis axis);
|
||||||
|
|
||||||
|
enum class JoypadKeyState
|
||||||
|
{
|
||||||
|
Pressed,
|
||||||
|
Released,
|
||||||
|
Count
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Key
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
|
||||||
|
Return,
|
||||||
|
LeftCtrl,
|
||||||
|
LeftAlt,
|
||||||
|
LeftShift,
|
||||||
|
Space,
|
||||||
|
Escape,
|
||||||
|
Left, Right, Up, Down,
|
||||||
|
_1, _2, _3, _4, _5, _6, _7, _8, _9, _0,
|
||||||
|
Count
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class MouseButton
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Middle,
|
||||||
|
Right,
|
||||||
|
Count
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class KeyState
|
||||||
|
{
|
||||||
|
Pressed,
|
||||||
|
Released,
|
||||||
|
Repeat,
|
||||||
|
Count
|
||||||
|
};
|
||||||
|
static_assert(Util::ecast(Key::Count) <= 64, "Cannot have more than 64 keys for bit-packing.");
|
||||||
|
|
||||||
|
struct TouchState
|
||||||
|
{
|
||||||
|
enum { PointerCount = 16 };
|
||||||
|
struct Pointer
|
||||||
|
{
|
||||||
|
unsigned id;
|
||||||
|
float start_x;
|
||||||
|
float start_y;
|
||||||
|
float last_x;
|
||||||
|
float last_y;
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
};
|
||||||
|
Pointer pointers[PointerCount] = {};
|
||||||
|
unsigned active_pointers = 0;
|
||||||
|
|
||||||
|
unsigned width;
|
||||||
|
unsigned height;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct JoypadState
|
||||||
|
{
|
||||||
|
bool is_button_pressed(JoypadKey key) const
|
||||||
|
{
|
||||||
|
return (button_mask & (1u << Util::ecast(key))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_axis(JoypadAxis a) const
|
||||||
|
{
|
||||||
|
return snapped_axis[Util::ecast(a)];
|
||||||
|
}
|
||||||
|
|
||||||
|
void snap_deadzone(float deadzone);
|
||||||
|
|
||||||
|
float raw_axis[Util::ecast(JoypadAxis::Count)] = {};
|
||||||
|
float snapped_axis[Util::ecast(JoypadAxis::Count)] = {};
|
||||||
|
uint32_t button_mask = 0;
|
||||||
|
uint32_t vid = 0;
|
||||||
|
uint32_t pid = 0;
|
||||||
|
};
|
||||||
|
static_assert(Util::ecast(JoypadKey::Count) <= 32, "Cannot have more than 32 joypad buttons.");
|
||||||
|
|
||||||
|
class InputTrackerHandler;
|
||||||
|
|
||||||
|
class InputTracker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void key_event(Key key, KeyState state);
|
||||||
|
void mouse_button_event(MouseButton button, double x, double y, bool pressed);
|
||||||
|
void mouse_button_event_normalized(MouseButton button, double x, double y, bool pressed);
|
||||||
|
void mouse_button_event(MouseButton button, bool pressed);
|
||||||
|
void mouse_move_event_absolute(double x, double y);
|
||||||
|
void mouse_move_event_absolute_normalized(double x, double y);
|
||||||
|
void mouse_move_event_relative(double x, double y);
|
||||||
|
void dispatch_current_state(double delta_time, InputTrackerHandler *override_handler = nullptr);
|
||||||
|
void orientation_event(quat rot);
|
||||||
|
void joypad_key_state(unsigned index, JoypadKey key, JoypadKeyState state);
|
||||||
|
void joyaxis_state(unsigned index, JoypadAxis axis, float value);
|
||||||
|
|
||||||
|
void on_touch_down(unsigned id, float x, float y);
|
||||||
|
void on_touch_move(unsigned id, float x, float y);
|
||||||
|
void on_touch_up(unsigned id, float x, float y);
|
||||||
|
|
||||||
|
void mouse_enter(double x, double y);
|
||||||
|
void mouse_leave();
|
||||||
|
|
||||||
|
bool key_pressed(Key key) const
|
||||||
|
{
|
||||||
|
return (key_state & (1ull << Util::ecast(key))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool joykey_pressed(unsigned index, JoypadKey key) const
|
||||||
|
{
|
||||||
|
if (index >= Joypads)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (joypads[index].button_mask & (1u << Util::ecast(key))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mouse_button_pressed(MouseButton button) const
|
||||||
|
{
|
||||||
|
return (mouse_button_state & (1ull << Util::ecast(button))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatch_touch_gesture();
|
||||||
|
|
||||||
|
void set_axis_deadzone(float deadzone)
|
||||||
|
{
|
||||||
|
axis_deadzone = deadzone;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_relative_mouse_rect(double x, double y, double width, double height)
|
||||||
|
{
|
||||||
|
mouse_relative_range_x = x;
|
||||||
|
mouse_relative_range_y = y;
|
||||||
|
mouse_relative_range_width = width;
|
||||||
|
mouse_relative_range_height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_relative_mouse_speed(double speed_x, double speed_y)
|
||||||
|
{
|
||||||
|
mouse_speed_x = speed_x;
|
||||||
|
mouse_speed_y = speed_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable_joypad(unsigned index, uint32_t vid, uint32_t pid);
|
||||||
|
void disable_joypad(unsigned index, uint32_t vid, uint32_t pid);
|
||||||
|
int find_vacant_joypad_index() const;
|
||||||
|
|
||||||
|
void set_touch_resolution(unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
touch.width = width;
|
||||||
|
touch.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_input_handler(InputTrackerHandler *handler_)
|
||||||
|
{
|
||||||
|
handler = handler_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To support dispatching input manager (i.e. polling) state from async threads.
|
||||||
|
std::mutex &get_lock();
|
||||||
|
|
||||||
|
enum { TouchCount = 16 };
|
||||||
|
enum { Joypads = 8 };
|
||||||
|
|
||||||
|
private:
|
||||||
|
InputTrackerHandler *handler = nullptr;
|
||||||
|
std::mutex dispatch_lock;
|
||||||
|
uint64_t key_state = 0;
|
||||||
|
uint8_t mouse_button_state = 0;
|
||||||
|
bool mouse_active = false;
|
||||||
|
|
||||||
|
double last_mouse_x = 0.0;
|
||||||
|
double last_mouse_y = 0.0;
|
||||||
|
double mouse_relative_range_x = 0.0;
|
||||||
|
double mouse_relative_range_y = 0.0;
|
||||||
|
double mouse_relative_range_width = DBL_MAX;
|
||||||
|
double mouse_relative_range_height = DBL_MAX;
|
||||||
|
double mouse_speed_x = 1.0;
|
||||||
|
double mouse_speed_y = 1.0;
|
||||||
|
|
||||||
|
uint8_t active_joypads = 0;
|
||||||
|
JoypadState joypads[Joypads] = {};
|
||||||
|
TouchState touch;
|
||||||
|
|
||||||
|
float axis_deadzone = 0.3f;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JoypadConnectionEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(JoypadConnectionEvent)
|
||||||
|
JoypadConnectionEvent(unsigned index_, bool connected_, uint32_t vid_, uint32_t pid_)
|
||||||
|
: index(index_), connected(connected_), vid(vid_), pid(pid_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_index() const
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_connected() const
|
||||||
|
{
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t get_vid() const
|
||||||
|
{
|
||||||
|
return vid;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t get_pid() const
|
||||||
|
{
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned index;
|
||||||
|
bool connected;
|
||||||
|
uint32_t vid, pid;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TouchGestureEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(TouchGestureEvent)
|
||||||
|
|
||||||
|
explicit TouchGestureEvent(const TouchState &state_)
|
||||||
|
: state(state_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const TouchState &get_state() const
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const TouchState &state;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TouchDownEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(TouchDownEvent)
|
||||||
|
|
||||||
|
TouchDownEvent(unsigned index_, unsigned id_,
|
||||||
|
float x_, float y_,
|
||||||
|
unsigned screen_width_, unsigned screen_height_)
|
||||||
|
: index(index_), id(id_),
|
||||||
|
x(x_), y(y_),
|
||||||
|
width(screen_width_), height(screen_height_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_x() const
|
||||||
|
{
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_y() const
|
||||||
|
{
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_index() const
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_id() const
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_screen_width() const
|
||||||
|
{
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_screen_height() const
|
||||||
|
{
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned index, id;
|
||||||
|
float x, y;
|
||||||
|
unsigned width, height;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TouchUpEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(TouchUpEvent)
|
||||||
|
|
||||||
|
TouchUpEvent(unsigned id_, float x_, float y_,
|
||||||
|
float start_x_, float start_y_,
|
||||||
|
unsigned screen_width_, unsigned screen_height_)
|
||||||
|
: id(id_), x(x_), y(y_),
|
||||||
|
start_x(start_x_), start_y(start_y_),
|
||||||
|
width(screen_width_), height(screen_height_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_x() const
|
||||||
|
{
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_y() const
|
||||||
|
{
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_start_x() const
|
||||||
|
{
|
||||||
|
return start_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_start_y() const
|
||||||
|
{
|
||||||
|
return start_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_id() const
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_screen_width() const
|
||||||
|
{
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_screen_height() const
|
||||||
|
{
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned id;
|
||||||
|
float x, y;
|
||||||
|
float start_x, start_y;
|
||||||
|
unsigned width, height;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JoypadButtonEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(JoypadButtonEvent)
|
||||||
|
|
||||||
|
JoypadButtonEvent(unsigned index_, JoypadKey key_, JoypadKeyState state_)
|
||||||
|
: index(index_), key(key_), state(state_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_index() const
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
JoypadKey get_key() const
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
JoypadKeyState get_state() const
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned index;
|
||||||
|
JoypadKey key;
|
||||||
|
JoypadKeyState state;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JoypadAxisEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(JoypadAxisEvent)
|
||||||
|
|
||||||
|
JoypadAxisEvent(unsigned index_, JoypadAxis axis_, float value_)
|
||||||
|
: index(index_), axis(axis_), value(value_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_index() const
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
JoypadAxis get_axis() const
|
||||||
|
{
|
||||||
|
return axis;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_value() const
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned index;
|
||||||
|
JoypadAxis axis;
|
||||||
|
float value;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KeyboardEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(KeyboardEvent)
|
||||||
|
|
||||||
|
KeyboardEvent(Key key_, KeyState state_)
|
||||||
|
: key(key_), state(state_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Key get_key() const
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyState get_key_state() const
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Key key;
|
||||||
|
KeyState state;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OrientationEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(OrientationEvent)
|
||||||
|
explicit OrientationEvent(const quat &rot_)
|
||||||
|
: rot(rot_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const quat &get_rotation() const
|
||||||
|
{
|
||||||
|
return rot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
quat rot;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MouseButtonEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(MouseButtonEvent)
|
||||||
|
|
||||||
|
MouseButtonEvent(MouseButton button_, double abs_x_, double abs_y_, bool pressed_)
|
||||||
|
: button(button_), abs_x(abs_x_), abs_y(abs_y_), pressed(pressed_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseButton get_button() const
|
||||||
|
{
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_abs_x() const
|
||||||
|
{
|
||||||
|
return abs_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_abs_y() const
|
||||||
|
{
|
||||||
|
return abs_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_pressed() const
|
||||||
|
{
|
||||||
|
return pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
MouseButton button;
|
||||||
|
double abs_x;
|
||||||
|
double abs_y;
|
||||||
|
bool pressed;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MouseMoveEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(MouseMoveEvent);
|
||||||
|
|
||||||
|
MouseMoveEvent(double delta_x_, double delta_y_, double abs_x_, double abs_y_,
|
||||||
|
uint64_t key_mask_, uint8_t btn_mask_)
|
||||||
|
: delta_x(delta_x_), delta_y(delta_y_),
|
||||||
|
abs_x(abs_x_), abs_y(abs_y_),
|
||||||
|
key_mask(key_mask_), btn_mask(btn_mask_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_mouse_button_pressed(MouseButton button) const
|
||||||
|
{
|
||||||
|
return (btn_mask & (1 << Util::ecast(button))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_key_pressed(Key key) const
|
||||||
|
{
|
||||||
|
return (key_mask & (1ull << Util::ecast(key))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_delta_x() const
|
||||||
|
{
|
||||||
|
return delta_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_delta_y() const
|
||||||
|
{
|
||||||
|
return delta_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_abs_x() const
|
||||||
|
{
|
||||||
|
return abs_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_abs_y() const
|
||||||
|
{
|
||||||
|
return abs_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
double delta_x, delta_y, abs_x, abs_y;
|
||||||
|
uint64_t key_mask;
|
||||||
|
uint8_t btn_mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JoypadStateEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(JoypadStateEvent)
|
||||||
|
|
||||||
|
JoypadStateEvent(uint8_t active_mask_, const JoypadState *states_,
|
||||||
|
unsigned count_, double delta_time_)
|
||||||
|
: states(states_), count(count_), delta_time(delta_time_), active_mask(active_mask_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_connected(unsigned index) const
|
||||||
|
{
|
||||||
|
if (index >= count)
|
||||||
|
return false;
|
||||||
|
return (active_mask & (1u << index)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_num_indices() const
|
||||||
|
{
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JoypadState &get_state(unsigned index) const
|
||||||
|
{
|
||||||
|
return states[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_delta_time() const
|
||||||
|
{
|
||||||
|
return delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const JoypadState *states;
|
||||||
|
unsigned count;
|
||||||
|
double delta_time;
|
||||||
|
uint8_t active_mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InputStateEvent : public Granite::Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GRANITE_EVENT_TYPE_DECL(InputStateEvent)
|
||||||
|
|
||||||
|
InputStateEvent(double abs_x_, double abs_y_,
|
||||||
|
double delta_time_, uint64_t key_mask_, uint8_t btn_mask_, bool mouse_active_)
|
||||||
|
: abs_x(abs_x_), abs_y(abs_y_),
|
||||||
|
delta_time(delta_time_), key_mask(key_mask_),
|
||||||
|
btn_mask(btn_mask_), mouse_active(mouse_active_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_delta_time() const
|
||||||
|
{
|
||||||
|
return delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_mouse_active() const
|
||||||
|
{
|
||||||
|
return mouse_active;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_mouse_button_pressed(MouseButton button) const
|
||||||
|
{
|
||||||
|
return (btn_mask & (1 << Util::ecast(button))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_key_pressed(Key key) const
|
||||||
|
{
|
||||||
|
return (key_mask & (1ull << Util::ecast(key))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_mouse_x() const
|
||||||
|
{
|
||||||
|
return abs_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get_mouse_y() const
|
||||||
|
{
|
||||||
|
return abs_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
double abs_x, abs_y;
|
||||||
|
double delta_time;
|
||||||
|
uint64_t key_mask;
|
||||||
|
uint8_t btn_mask;
|
||||||
|
bool mouse_active;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InputTrackerHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~InputTrackerHandler() = default;
|
||||||
|
virtual void dispatch(const TouchDownEvent &e) = 0;
|
||||||
|
virtual void dispatch(const TouchUpEvent &e) = 0;
|
||||||
|
virtual void dispatch(const TouchGestureEvent &e) = 0;
|
||||||
|
virtual void dispatch(const JoypadButtonEvent &e) = 0;
|
||||||
|
virtual void dispatch(const JoypadAxisEvent &e) = 0;
|
||||||
|
virtual void dispatch(const KeyboardEvent &e) = 0;
|
||||||
|
virtual void dispatch(const OrientationEvent &e) = 0;
|
||||||
|
virtual void dispatch(const MouseButtonEvent &e) = 0;
|
||||||
|
virtual void dispatch(const MouseMoveEvent &e) = 0;
|
||||||
|
virtual void dispatch(const JoypadStateEvent &e) = 0;
|
||||||
|
virtual void dispatch(const InputStateEvent &e) = 0;
|
||||||
|
virtual void dispatch(const JoypadConnectionEvent &e) = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "input_sdl.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
bool InputTrackerSDL::init(InputTracker &tracker, const Dispatcher &dispatcher)
|
||||||
|
{
|
||||||
|
// Open existing gamepads.
|
||||||
|
int num_pads = 0;
|
||||||
|
SDL_JoystickID *gamepad_ids = SDL_GetGamepads(&num_pads);
|
||||||
|
for (int i = 0; i < num_pads; i++)
|
||||||
|
add_gamepad(gamepad_ids[i], tracker, dispatcher);
|
||||||
|
if (gamepad_ids)
|
||||||
|
SDL_free(gamepad_ids);
|
||||||
|
|
||||||
|
// Poll these separately, inline in poll_input().
|
||||||
|
SDL_SetGamepadEventsEnabled(false);
|
||||||
|
SDL_SetJoystickEventsEnabled(false);
|
||||||
|
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_ADDED, true);
|
||||||
|
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_REMOVED, true);
|
||||||
|
SDL_SetEventEnabled(SDL_EVENT_JOYSTICK_UPDATE_COMPLETE, false);
|
||||||
|
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_UPDATE_COMPLETE, false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTrackerSDL::update(InputTracker &tracker)
|
||||||
|
{
|
||||||
|
SDL_UpdateGamepads();
|
||||||
|
|
||||||
|
for (int i = 0; i < int(InputTracker::Joypads); i++)
|
||||||
|
{
|
||||||
|
auto *pad = pads[i];
|
||||||
|
if (!pad)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
static const struct
|
||||||
|
{
|
||||||
|
JoypadKey gkey;
|
||||||
|
SDL_GamepadButton sdl;
|
||||||
|
} buttons[] = {
|
||||||
|
{ JoypadKey::Left, SDL_GAMEPAD_BUTTON_DPAD_LEFT },
|
||||||
|
{ JoypadKey::Right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT },
|
||||||
|
{ JoypadKey::Up, SDL_GAMEPAD_BUTTON_DPAD_UP },
|
||||||
|
{ JoypadKey::Down, SDL_GAMEPAD_BUTTON_DPAD_DOWN },
|
||||||
|
{ JoypadKey::Start, SDL_GAMEPAD_BUTTON_START },
|
||||||
|
{ JoypadKey::Select, SDL_GAMEPAD_BUTTON_BACK },
|
||||||
|
{ JoypadKey::East, SDL_GAMEPAD_BUTTON_EAST },
|
||||||
|
{ JoypadKey::West, SDL_GAMEPAD_BUTTON_WEST },
|
||||||
|
{ JoypadKey::North, SDL_GAMEPAD_BUTTON_NORTH },
|
||||||
|
{ JoypadKey::South, SDL_GAMEPAD_BUTTON_SOUTH },
|
||||||
|
{ JoypadKey::LeftShoulder, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER },
|
||||||
|
{ JoypadKey::RightShoulder, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
|
||||||
|
{ JoypadKey::LeftThumb, SDL_GAMEPAD_BUTTON_LEFT_STICK },
|
||||||
|
{ JoypadKey::RightThumb, SDL_GAMEPAD_BUTTON_RIGHT_STICK },
|
||||||
|
{ JoypadKey::Mode, SDL_GAMEPAD_BUTTON_GUIDE },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto &b : buttons)
|
||||||
|
{
|
||||||
|
tracker.joypad_key_state(i, b.gkey,
|
||||||
|
SDL_GetGamepadButton(pad, b.sdl) ?
|
||||||
|
JoypadKeyState::Pressed : JoypadKeyState::Released);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct
|
||||||
|
{
|
||||||
|
JoypadAxis gaxis;
|
||||||
|
SDL_GamepadAxis sdl;
|
||||||
|
} axes[] = {
|
||||||
|
{ JoypadAxis::LeftX, SDL_GAMEPAD_AXIS_LEFTX },
|
||||||
|
{ JoypadAxis::LeftY, SDL_GAMEPAD_AXIS_LEFTY },
|
||||||
|
{ JoypadAxis::RightX, SDL_GAMEPAD_AXIS_RIGHTX },
|
||||||
|
{ JoypadAxis::RightY, SDL_GAMEPAD_AXIS_RIGHTY },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto &a : axes)
|
||||||
|
{
|
||||||
|
float value = float(SDL_GetGamepadAxis(pad, a.sdl) - SDL_JOYSTICK_AXIS_MIN) /
|
||||||
|
float(SDL_JOYSTICK_AXIS_MAX - SDL_JOYSTICK_AXIS_MIN);
|
||||||
|
value = 2.0f * value - 1.0f;
|
||||||
|
tracker.joyaxis_state(i, a.gaxis, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.joyaxis_state(i, JoypadAxis::LeftTrigger,
|
||||||
|
float(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) /
|
||||||
|
float(SDL_JOYSTICK_AXIS_MAX));
|
||||||
|
|
||||||
|
tracker.joyaxis_state(i, JoypadAxis::RightTrigger,
|
||||||
|
float(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) /
|
||||||
|
float(SDL_JOYSTICK_AXIS_MAX));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTrackerSDL::close()
|
||||||
|
{
|
||||||
|
for (auto *pad : pads)
|
||||||
|
if (pad)
|
||||||
|
SDL_CloseGamepad(pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputTrackerSDL::process_sdl_event(const SDL_Event &e, InputTracker &tracker,
|
||||||
|
const InputTrackerSDL::Dispatcher &dispatcher)
|
||||||
|
{
|
||||||
|
switch (e.type)
|
||||||
|
{
|
||||||
|
case SDL_EVENT_GAMEPAD_ADDED:
|
||||||
|
{
|
||||||
|
add_gamepad(e.gdevice.which, tracker, dispatcher);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||||
|
{
|
||||||
|
remove_gamepad(e.gdevice.which, tracker, dispatcher);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTrackerSDL::add_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher)
|
||||||
|
{
|
||||||
|
int player_index = SDL_GetJoystickPlayerIndexForID(id);
|
||||||
|
if (player_index >= 0 && player_index < int(InputTracker::Joypads) && !pads[player_index])
|
||||||
|
{
|
||||||
|
uint32_t vid = SDL_GetGamepadVendorForID(id);
|
||||||
|
uint32_t pid = SDL_GetGamepadProductForID(id);
|
||||||
|
const char *name = SDL_GetGamepadNameForID(id);
|
||||||
|
LOGI("Plugging in controller: \"%s\" (%u/%u).\n", name, vid, pid);
|
||||||
|
const char *mapping = SDL_GetGamepadMappingForID(id);
|
||||||
|
LOGI(" Using mapping: \"%s\"\n", mapping);
|
||||||
|
pads[player_index] = SDL_OpenGamepad(id);
|
||||||
|
ids[player_index] = id;
|
||||||
|
dispatcher([player_index, vid, pid, &tracker]() {
|
||||||
|
tracker.enable_joypad(player_index, vid, pid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTrackerSDL::remove_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < int(InputTracker::Joypads); i++)
|
||||||
|
{
|
||||||
|
if (pads[i] && ids[i] == id)
|
||||||
|
{
|
||||||
|
uint32_t vid = SDL_GetGamepadVendor(pads[i]);
|
||||||
|
uint32_t pid = SDL_GetGamepadProduct(pads[i]);
|
||||||
|
SDL_CloseGamepad(pads[i]);
|
||||||
|
pads[i] = nullptr;
|
||||||
|
ids[i] = 0;
|
||||||
|
dispatcher([i, vid, pid, &tracker]() {
|
||||||
|
tracker.disable_joypad(i, vid, pid);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include "input.hpp"
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class InputTrackerSDL
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using Dispatcher = std::function<void (std::function<void ()>)>;
|
||||||
|
bool init(InputTracker &tracker, const Dispatcher &dispatcher);
|
||||||
|
void close();
|
||||||
|
bool process_sdl_event(const SDL_Event &event, InputTracker &tracker, const Dispatcher &dispatcher);
|
||||||
|
void update(InputTracker &tracker);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_Gamepad *pads[InputTracker::Joypads] = {};
|
||||||
|
SDL_JoystickID ids[InputTracker::Joypads] = {};
|
||||||
|
void add_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher);
|
||||||
|
void remove_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher);
|
||||||
|
};
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
# Must be static due to Granite::application_create() feedback.
|
||||||
|
|
||||||
|
if (ANDROID)
|
||||||
|
add_granite_internal_static_lib(granite-platform application_android.cpp)
|
||||||
|
find_package(game-activity REQUIRED CONFIG)
|
||||||
|
find_package(games-controller REQUIRED CONFIG)
|
||||||
|
find_package(games-frame-pacing REQUIRED CONFIG)
|
||||||
|
target_link_libraries(granite-platform PRIVATE granite-rapidjson
|
||||||
|
game-activity::game-activity_static
|
||||||
|
games-controller::paddleboat games-frame-pacing::swappy)
|
||||||
|
|
||||||
|
target_compile_definitions(granite-platform PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
|
||||||
|
if (GRANITE_ANDROID_SWAPPY)
|
||||||
|
target_compile_definitions(granite-platform PRIVATE HAVE_SWAPPY)
|
||||||
|
endif()
|
||||||
|
if (GRANITE_ANDROID_APK_FILESYSTEM)
|
||||||
|
target_compile_definitions(granite-platform PRIVATE ANDROID_APK_FILESYSTEM)
|
||||||
|
endif()
|
||||||
|
elseif (${GRANITE_PLATFORM} MATCHES "libretro")
|
||||||
|
add_granite_internal_static_lib(granite-platform
|
||||||
|
application_libretro.cpp
|
||||||
|
application_libretro_utils.cpp
|
||||||
|
application_libretro_utils.hpp)
|
||||||
|
target_compile_definitions(granite-platform PUBLIC HAVE_LIBRETRO)
|
||||||
|
target_include_directories(granite-platform PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/libretro)
|
||||||
|
elseif (${GRANITE_PLATFORM} MATCHES "SDL")
|
||||||
|
add_granite_internal_static_lib(granite-platform application_sdl3.cpp)
|
||||||
|
|
||||||
|
if (GRANITE_SYSTEM_SDL)
|
||||||
|
find_package(SDL3 REQUIRED)
|
||||||
|
target_link_libraries(granite-platform PRIVATE SDL3::SDL3-shared)
|
||||||
|
else()
|
||||||
|
target_link_libraries(granite-platform PRIVATE SDL3-static)
|
||||||
|
if (NOT WIN32)
|
||||||
|
target_link_libraries(granite-platform PRIVATE dl)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
target_link_libraries(granite-platform PRIVATE granite-input-sdl)
|
||||||
|
elseif (${GRANITE_PLATFORM} MATCHES "headless")
|
||||||
|
add_granite_internal_static_lib(granite-platform application_headless_wrapper.cpp)
|
||||||
|
elseif (${GRANITE_PLATFORM} MATCHES "null")
|
||||||
|
add_granite_internal_static_lib(granite-platform application_null.cpp)
|
||||||
|
else()
|
||||||
|
message(FATAL "GRANITE_PLATFORM is not set.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT (${GRANITE_PLATFORM} MATCHES "null"))
|
||||||
|
target_link_libraries(granite-platform PRIVATE granite-application-interface-query granite-application granite-input granite-application-global-init)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (GRANITE_AUDIO)
|
||||||
|
target_link_libraries(granite-platform PRIVATE granite-audio)
|
||||||
|
endif()
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = 'net.themaister.granite'
|
||||||
|
compileSdkVersion = 34
|
||||||
|
buildFeatures.prefab = true
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion = 26
|
||||||
|
targetSdkVersion = 34
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||||
|
implementation 'com.google.android.material:material:1.7.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
implementation 'androidx.core:core:1.9.0'
|
||||||
|
implementation 'androidx.games:games-activity:4.0.0'
|
||||||
|
}
|
||||||
crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/external_layers/README.txt
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
Place {arm64-v8a,armeabi-v7a}/libVkLayer_*.so here to have it bundled in the APK.
|
||||||
crates/pyrowave-sys/vendor/pyrowave/Granite/application/platforms/android/gradle/AndroidManifest.xml
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:versionCode="$$VERSION_CODE$$"
|
||||||
|
android:versionName="$$VERSION_NAME$$">
|
||||||
|
|
||||||
|
<!-- Require Vulkan 1.1 -->
|
||||||
|
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true"/>
|
||||||
|
<uses-feature android:name="android.hardware.vulkan.level" android:version="0" android:required="true"/>
|
||||||
|
|
||||||
|
<application android:label="@string/app_name" android:icon="@drawable/$$ICON$$">
|
||||||
|
<activity android:name="$$ACTIVITY_NAME$$"
|
||||||
|
android:theme="@style/Application.Fullscreen"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="landscape"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
<meta-data android:name="android.app.lib_name" android:value="$$NATIVE_TARGET$$" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
</manifest>
|
||||||
Vendored
+100
@@ -0,0 +1,100 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = '$$NAMESPACE$$'
|
||||||
|
compileSdkVersion = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion = 26
|
||||||
|
targetSdkVersion = 34
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters = [$$ABIS$$]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ndkVersion = '29.0.14206865'
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments = ["-DANDROID_TOOLCHAIN=clang",
|
||||||
|
"-DANDROID_STL=c++_shared",
|
||||||
|
"-DANDROID_ARM_MODE=arm",
|
||||||
|
"-DANDROID_CPP_FEATURES=exceptions",
|
||||||
|
"-DGRANITE_SHADER_COMPILER_OPTIMIZE=$$SHADER_OPTIMIZE$$",
|
||||||
|
"-DGRANITE_VULKAN_FOSSILIZE=$$FOSSILIZE$$",
|
||||||
|
"-DGRANITE_ANDROID_SWAPPY=$$SWAPPY$$",
|
||||||
|
"-DCMAKE_BUILD_TYPE=Debug",
|
||||||
|
"-DANDROID_PLATFORM=android-26",
|
||||||
|
"-DGRANITE_SHIPPING=ON",
|
||||||
|
"-DGRANITE_AUDIO=$$AUDIO$$",
|
||||||
|
"-DGRANITE_BULLET=$$PHYSICS$$",
|
||||||
|
"-DANDROID_ARM_NEON=ON",
|
||||||
|
"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF",
|
||||||
|
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
|
||||||
|
|
||||||
|
targets = ["$$TARGET$$"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jniDebuggable = true
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments = ["-DANDROID_TOOLCHAIN=clang",
|
||||||
|
"-DANDROID_STL=c++_shared",
|
||||||
|
"-DANDROID_ARM_MODE=arm",
|
||||||
|
"-DANDROID_CPP_FEATURES=exceptions",
|
||||||
|
"-DGRANITE_SHADER_COMPILER_OPTIMIZE=$$SHADER_OPTIMIZE$$",
|
||||||
|
"-DGRANITE_VULKAN_FOSSILIZE=$$FOSSILIZE$$",
|
||||||
|
"-DGRANITE_ANDROID_SWAPPY=$$SWAPPY$$",
|
||||||
|
"-DCMAKE_BUILD_TYPE=Release",
|
||||||
|
"-DANDROID_PLATFORM=android-26",
|
||||||
|
"-DGRANITE_SHIPPING=ON",
|
||||||
|
"-DGRANITE_AUDIO=$$AUDIO$$",
|
||||||
|
"-DGRANITE_BULLET=$$PHYSICS$$",
|
||||||
|
"-DANDROID_ARM_NEON=ON",
|
||||||
|
"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF",
|
||||||
|
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
|
||||||
|
|
||||||
|
targets = ["$$TARGET$$"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debuggable = true
|
||||||
|
signingConfig = signingConfigs.debug
|
||||||
|
jniDebuggable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
manifest.srcFile 'AndroidManifest.xml'
|
||||||
|
resources.srcDirs = ['res']
|
||||||
|
res.srcDirs = ['res']
|
||||||
|
assets.srcDirs = ['$$ASSETS$$', '$$GRANITE_ASSETS$$']
|
||||||
|
jniLibs.srcDirs = ['$$EXTERNAL_JNI$$']
|
||||||
|
$$GRANITE_AUX_ASSETS$$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path = "$$CMAKELISTS$$"
|
||||||
|
version = "3.22.0+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
prefab = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api project(':granite')
|
||||||
|
implementation 'androidx.games:games-activity:4.0.0'
|
||||||
|
implementation 'androidx.games:games-controller:2.0.2'
|
||||||
|
implementation 'androidx.games:games-frame-pacing:2.1.3'
|
||||||
|
$$EXTRA_DEPENDENCIES$$
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
android.useAndroidX=true
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
include 'granite'
|
||||||
|
project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
|
||||||
|
include '$$APP$$'
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
include 'granite'
|
||||||
|
project(':granite').projectDir = file('$$GRANITE_ANDROID_ACTIVITY_PATH$$')
|
||||||
|
include 'custom'
|
||||||
|
project(':custom').projectDir = file('$$ANDROID_ACTIVITY_PATH$$')
|
||||||
|
include '$$APP$$'
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:9.0.0-rc01'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.themaister.granite;
|
||||||
|
|
||||||
|
import com.google.androidgamesdk.GameActivity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Display;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat;
|
||||||
|
|
||||||
|
public class GraniteActivity extends GameActivity
|
||||||
|
{
|
||||||
|
private final static String TAG = "Granite";
|
||||||
|
|
||||||
|
private void hideSystemUI()
|
||||||
|
{
|
||||||
|
// This will put the game behind any cutouts and waterfalls on devices which have
|
||||||
|
// them, so the corresponding insets will be non-zero.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||||
|
{
|
||||||
|
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||||
|
|
||||||
|
// From API 30 onwards, this is the recommended way to hide the system UI, rather than
|
||||||
|
// using View.setSystemUiVisibility.
|
||||||
|
View decorView = getWindow().getDecorView();
|
||||||
|
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(),
|
||||||
|
decorView);
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||||
|
controller.hide(WindowInsetsCompat.Type.displayCutout());
|
||||||
|
controller.setSystemBarsBehavior(
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||||
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedState)
|
||||||
|
{
|
||||||
|
// When true, the app will fit inside any system UI windows.
|
||||||
|
// When false, we render behind any system UI windows.
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
hideSystemUI();
|
||||||
|
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||||
|
super.onCreate(savedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus)
|
||||||
|
{
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus && Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
|
||||||
|
hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed()
|
||||||
|
{
|
||||||
|
// Do nothing. We catch this inside native code instead.
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDisplayRotation()
|
||||||
|
{
|
||||||
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
|
if (display == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return display.getRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAudioNativeSampleRate()
|
||||||
|
{
|
||||||
|
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
if (am == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
|
||||||
|
if (sampleRate == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int rate = Integer.parseInt(sampleRate);
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAudioNativeBlockFrames()
|
||||||
|
{
|
||||||
|
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
if (am == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
String frames = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
|
||||||
|
if (frames == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int count = Integer.parseInt(frames);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCommandLineArgument(String key)
|
||||||
|
{
|
||||||
|
Intent intent = getIntent();
|
||||||
|
if (intent == null)
|
||||||
|
return "";
|
||||||
|
String extra = intent.getStringExtra(key);
|
||||||
|
if (extra == null)
|
||||||
|
return "";
|
||||||
|
return extra;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Application.Fullscreen" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
+1363
File diff suppressed because it is too large
Load Diff
+686
@@ -0,0 +1,686 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define NOMINMAX
|
||||||
|
#include "application.hpp"
|
||||||
|
#include "application_events.hpp"
|
||||||
|
#include "application_wsi.hpp"
|
||||||
|
#include "vulkan_headers.hpp"
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include "stb_image_write.h"
|
||||||
|
#include "cli_parser.hpp"
|
||||||
|
#include "os_filesystem.hpp"
|
||||||
|
#include "rapidjson_wrapper.hpp"
|
||||||
|
#include <limits.h>
|
||||||
|
#include <cmath>
|
||||||
|
#include "thread_group.hpp"
|
||||||
|
#include "global_managers_init.hpp"
|
||||||
|
#include "path_utils.hpp"
|
||||||
|
#include "thread_group.hpp"
|
||||||
|
#include "asset_manager.hpp"
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_FFMPEG
|
||||||
|
#include "ffmpeg_encode.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
#include "audio_interface.hpp"
|
||||||
|
#include "audio_mixer.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace rapidjson;
|
||||||
|
using namespace Vulkan;
|
||||||
|
using namespace Util;
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
struct WSIPlatformHeadless : Granite::GraniteWSIPlatform
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
~WSIPlatformHeadless() override
|
||||||
|
{
|
||||||
|
release_resources();
|
||||||
|
}
|
||||||
|
|
||||||
|
void release_resources() override
|
||||||
|
{
|
||||||
|
for (auto &t : swapchain_tasks)
|
||||||
|
t.reset();
|
||||||
|
if (last_task_dependency)
|
||||||
|
last_task_dependency->wait();
|
||||||
|
last_task_dependency.reset();
|
||||||
|
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
{
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
swapchain_images.clear();
|
||||||
|
readback_buffers.clear();
|
||||||
|
acquire_semaphore.clear();
|
||||||
|
#ifdef HAVE_GRANITE_FFMPEG
|
||||||
|
ycbcr_pipelines.clear();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool alive(Vulkan::WSI &) override
|
||||||
|
{
|
||||||
|
return frames < max_frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll_input() override
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||||
|
get_input_tracker().dispatch_current_state(get_frame_timer().get_frame_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||||
|
get_input_tracker().dispatch_current_state(0.0, override_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable_png_readback(std::string base_path)
|
||||||
|
{
|
||||||
|
png_readback = std::move(base_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const char *> get_instance_extensions() override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
VkSurfaceKHR create_surface(VkInstance, VkPhysicalDevice) override
|
||||||
|
{
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t get_surface_width() override
|
||||||
|
{
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t get_surface_height() override
|
||||||
|
{
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify_resize(unsigned width_, unsigned height_)
|
||||||
|
{
|
||||||
|
resize = true;
|
||||||
|
width = width_;
|
||||||
|
height = height_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_max_frames(unsigned max_frames_)
|
||||||
|
{
|
||||||
|
max_frames = max_frames_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_external_swapchain() override
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool init(unsigned width_, unsigned height_)
|
||||||
|
{
|
||||||
|
width = width_;
|
||||||
|
height = height_;
|
||||||
|
if (!Context::init_loader(nullptr))
|
||||||
|
{
|
||||||
|
LOGE("Failed to initialize Vulkan loader.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
{
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool init_headless(Application *app_)
|
||||||
|
{
|
||||||
|
app = app_;
|
||||||
|
|
||||||
|
auto context = Util::make_handle<Context>();
|
||||||
|
|
||||||
|
Context::SystemHandles system_handles;
|
||||||
|
system_handles.filesystem = GRANITE_FILESYSTEM();
|
||||||
|
system_handles.thread_group = GRANITE_THREAD_GROUP();
|
||||||
|
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
|
||||||
|
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
|
||||||
|
context->set_system_handles(system_handles);
|
||||||
|
|
||||||
|
context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
|
||||||
|
const char *khr_surface = VK_KHR_SURFACE_EXTENSION_NAME;
|
||||||
|
const char *khr_swapchain = VK_KHR_SWAPCHAIN_EXTENSION_NAME;
|
||||||
|
|
||||||
|
auto name = app->get_name();
|
||||||
|
if (name.empty())
|
||||||
|
name = Path::basename(Path::get_executable_path());
|
||||||
|
VkApplicationInfo app_info = { VK_STRUCTURE_TYPE_APPLICATION_INFO };
|
||||||
|
app_info.pEngineName = "Granite";
|
||||||
|
app_info.pApplicationName = name.empty() ? "Granite" : name.c_str();
|
||||||
|
app_info.apiVersion = VK_API_VERSION_1_1;
|
||||||
|
context->set_application_info(&app_info);
|
||||||
|
|
||||||
|
if (!context->init_instance_and_device(&khr_surface, 1, &khr_swapchain, 1))
|
||||||
|
return false;
|
||||||
|
if (!app->init_wsi(std::move(context)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto &device = app->get_wsi().get_device();
|
||||||
|
|
||||||
|
auto info = ImageCreateInfo::render_target(width, height, VK_FORMAT_R8G8B8A8_SRGB);
|
||||||
|
info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||||
|
info.misc |= Vulkan::IMAGE_MISC_MUTABLE_SRGB_BIT;
|
||||||
|
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
|
||||||
|
BufferCreateInfo readback = {};
|
||||||
|
readback.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||||
|
readback.domain = BufferDomain::CachedHost;
|
||||||
|
readback.size = width * height * sizeof(uint32_t);
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < SwapchainImages; i++)
|
||||||
|
{
|
||||||
|
swapchain_images.push_back(device.create_image(info, nullptr));
|
||||||
|
readback_buffers.push_back(device.create_buffer(readback, nullptr));
|
||||||
|
acquire_semaphore.emplace_back(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target present layouts to be more accurate for timing in case PRESENT_SRC forces decompress,
|
||||||
|
// and also makes sure pipeline caches are valid w.r.t render passes.
|
||||||
|
for (auto &swap : swapchain_images)
|
||||||
|
swap->set_swapchain_layout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||||
|
|
||||||
|
app->get_wsi().init_external_swapchain(swapchain_images);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_FFMPEG
|
||||||
|
void init_headless_recording(std::string path)
|
||||||
|
{
|
||||||
|
#ifndef HAVE_GRANITE_RENDERER
|
||||||
|
LOGE("Need to include system handles in build to encode.\n");
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
video_encode_path = std::move(path);
|
||||||
|
VideoEncoder::Options enc_opts = {};
|
||||||
|
enc_opts.width = width;
|
||||||
|
enc_opts.height = height;
|
||||||
|
|
||||||
|
double frame_rate = std::round(1.0 / time_step);
|
||||||
|
enc_opts.frame_timebase.num = 1;
|
||||||
|
enc_opts.frame_timebase.den = int(frame_rate);
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
enc_opts.walltime_to_pts = true;
|
||||||
|
record_stream.reset(Audio::create_default_audio_record_backend("headless", 44100.0f, 2));
|
||||||
|
if (record_stream)
|
||||||
|
encoder.set_audio_record_stream(record_stream.get());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!encoder.init(&app->get_wsi().get_device(), video_encode_path.c_str(), enc_opts))
|
||||||
|
{
|
||||||
|
LOGE("Failed to initialize encoder.\n");
|
||||||
|
video_encode_path.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_RENDERER
|
||||||
|
for (unsigned i = 0; i < SwapchainImages; i++)
|
||||||
|
{
|
||||||
|
auto &device = app->get_wsi().get_device();
|
||||||
|
FFmpegEncode::Shaders<> shaders;
|
||||||
|
|
||||||
|
shaders.scaler = device.get_shader_manager().register_compute(
|
||||||
|
"builtin://shaders/util/scaler.comp")->register_variant({})->get_program();
|
||||||
|
ycbcr_pipelines.push_back(encoder.create_ycbcr_pipeline(shaders));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
record_stream->start();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void set_time_step(double t)
|
||||||
|
{
|
||||||
|
time_step = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void begin_frame()
|
||||||
|
{
|
||||||
|
auto &wsi = app->get_wsi();
|
||||||
|
wsi.set_external_frame(frame_index, std::move(acquire_semaphore[frame_index]), time_step);
|
||||||
|
acquire_semaphore[frame_index] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void end_frame()
|
||||||
|
{
|
||||||
|
auto &wsi = app->get_wsi();
|
||||||
|
auto &device = wsi.get_device();
|
||||||
|
auto release_semaphore = wsi.consume_external_release_semaphore();
|
||||||
|
|
||||||
|
if (release_semaphore && release_semaphore->get_semaphore() != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
if (swapchain_tasks[frame_index])
|
||||||
|
{
|
||||||
|
swapchain_tasks[frame_index]->wait();
|
||||||
|
swapchain_tasks[frame_index].reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire_semaphore[frame_index] = {};
|
||||||
|
|
||||||
|
if (!next_readback_path.empty() || !png_readback.empty())
|
||||||
|
{
|
||||||
|
OwnershipTransferInfo transfer_info = {};
|
||||||
|
transfer_info.old_queue = wsi.get_current_present_queue_type();
|
||||||
|
transfer_info.new_queue = CommandBuffer::Type::AsyncTransfer;
|
||||||
|
transfer_info.old_image_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||||
|
transfer_info.new_image_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||||
|
transfer_info.dst_pipeline_stage = VK_PIPELINE_STAGE_2_COPY_BIT;
|
||||||
|
transfer_info.dst_access = VK_ACCESS_TRANSFER_READ_BIT;
|
||||||
|
auto cmd = request_command_buffer_with_ownership_transfer(device, *swapchain_images[frame_index],
|
||||||
|
transfer_info, release_semaphore);
|
||||||
|
|
||||||
|
cmd->copy_image_to_buffer(*readback_buffers[frame_index], *swapchain_images[frame_index],
|
||||||
|
0, {}, {width, height, 1},
|
||||||
|
0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1});
|
||||||
|
|
||||||
|
cmd->barrier(VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
|
VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||||
|
|
||||||
|
Fence readback_fence;
|
||||||
|
device.submit(cmd, &readback_fence, 1, &acquire_semaphore[frame_index]);
|
||||||
|
|
||||||
|
if (!next_readback_path.empty())
|
||||||
|
{
|
||||||
|
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
|
||||||
|
[this, readback_fence, index = frame_index, frame = this->frames, p = std::make_unique<std::string>(next_readback_path)]() mutable {
|
||||||
|
readback_fence->wait();
|
||||||
|
dump_frame_single(*p, frame, index);
|
||||||
|
});
|
||||||
|
next_readback_path.clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
|
||||||
|
[this, readback_fence, index = frame_index, frame = this->frames]() mutable {
|
||||||
|
readback_fence->wait();
|
||||||
|
dump_frame(frame, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef HAVE_GRANITE_FFMPEG
|
||||||
|
else if (!video_encode_path.empty())
|
||||||
|
{
|
||||||
|
auto pts = encoder.sample_realtime_pts();
|
||||||
|
|
||||||
|
OwnershipTransferInfo transfer_info = {};
|
||||||
|
transfer_info.old_queue = wsi.get_current_present_queue_type();
|
||||||
|
transfer_info.new_queue = CommandBuffer::Type::AsyncCompute;
|
||||||
|
transfer_info.old_image_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||||
|
transfer_info.new_image_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
|
||||||
|
transfer_info.dst_pipeline_stage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||||
|
transfer_info.dst_access = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
|
||||||
|
auto cmd = request_command_buffer_with_ownership_transfer(device, *swapchain_images[frame_index],
|
||||||
|
transfer_info, release_semaphore);
|
||||||
|
|
||||||
|
encoder.process_rgb(*cmd, ycbcr_pipelines[frame_index], swapchain_images[frame_index]->get_view());
|
||||||
|
encoder.submit_process_rgb(cmd, ycbcr_pipelines[frame_index]);
|
||||||
|
|
||||||
|
acquire_semaphore[frame_index] = device.request_semaphore(VK_SEMAPHORE_TYPE_BINARY);
|
||||||
|
device.submit_empty(CommandBuffer::Type::AsyncCompute, nullptr, acquire_semaphore[frame_index].get());
|
||||||
|
|
||||||
|
swapchain_tasks[frame_index] = GRANITE_THREAD_GROUP()->create_task(
|
||||||
|
[this, index = frame_index, pts]() mutable {
|
||||||
|
if (!encoder.encode_frame(ycbcr_pipelines[index], pts))
|
||||||
|
LOGE("Failed to push frame to encoder.\n");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Do nothing.
|
||||||
|
acquire_semaphore[frame_index] = std::move(release_semaphore);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swapchain_tasks[frame_index])
|
||||||
|
{
|
||||||
|
swapchain_tasks[frame_index]->set_desc("application-headless-readback");
|
||||||
|
swapchain_tasks[frame_index]->set_task_class(TaskClass::Background);
|
||||||
|
|
||||||
|
if (last_task_dependency)
|
||||||
|
GRANITE_THREAD_GROUP()->add_dependency(*swapchain_tasks[frame_index], *last_task_dependency);
|
||||||
|
|
||||||
|
// Add a dummy task that only serves to chain dependencies.
|
||||||
|
last_task_dependency = GRANITE_THREAD_GROUP()->create_task();
|
||||||
|
last_task_dependency->set_task_class(TaskClass::Background);
|
||||||
|
GRANITE_THREAD_GROUP()->add_dependency(*last_task_dependency, *swapchain_tasks[frame_index]);
|
||||||
|
swapchain_tasks[frame_index]->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
release_semaphore = {};
|
||||||
|
frame_index = (frame_index + 1) % SwapchainImages;
|
||||||
|
frames++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_next_readback(std::string path)
|
||||||
|
{
|
||||||
|
next_readback_path = std::move(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void wait_threads()
|
||||||
|
{
|
||||||
|
GRANITE_THREAD_GROUP()->wait_idle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
unsigned frames = 0;
|
||||||
|
unsigned max_frames = UINT_MAX;
|
||||||
|
unsigned frame_index = 0;
|
||||||
|
double time_step = 0.01;
|
||||||
|
std::string png_readback;
|
||||||
|
std::string video_encode_path;
|
||||||
|
enum { SwapchainImages = 4 };
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
std::unique_ptr<Audio::RecordStream> record_stream;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::vector<ImageHandle> swapchain_images;
|
||||||
|
std::vector<BufferHandle> readback_buffers;
|
||||||
|
std::vector<Semaphore> acquire_semaphore;
|
||||||
|
std::string next_readback_path;
|
||||||
|
TaskGroupHandle swapchain_tasks[SwapchainImages];
|
||||||
|
TaskGroupHandle last_task_dependency;
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_FFMPEG
|
||||||
|
VideoEncoder encoder;
|
||||||
|
std::vector<VideoEncoder::YCbCrPipeline> ycbcr_pipelines;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void dump_frame_single(const std::string &path, unsigned frame, unsigned index)
|
||||||
|
{
|
||||||
|
auto &wsi = app->get_wsi();
|
||||||
|
auto &device = wsi.get_device();
|
||||||
|
|
||||||
|
LOGI("Dumping frame: %u (index: %u)\n", frame, index);
|
||||||
|
|
||||||
|
auto *ptr = static_cast<uint32_t *>(device.map_host_buffer(*readback_buffers[index], MEMORY_ACCESS_READ_WRITE_BIT));
|
||||||
|
for (unsigned i = 0; i < width * height; i++)
|
||||||
|
ptr[i] |= 0xff000000u;
|
||||||
|
|
||||||
|
if (!stbi_write_png(path.c_str(), width, height, 4, ptr, width * 4))
|
||||||
|
LOGE("Failed to write PNG to disk.\n");
|
||||||
|
device.unmap_host_buffer(*readback_buffers[index], MEMORY_ACCESS_READ_WRITE_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_frame(unsigned frame, unsigned index)
|
||||||
|
{
|
||||||
|
char buffer[64];
|
||||||
|
sprintf(buffer, "_%05u.png", frame);
|
||||||
|
auto path = png_readback + buffer;
|
||||||
|
dump_frame_single(path, frame, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Application *app = nullptr;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_help()
|
||||||
|
{
|
||||||
|
LOGI("[--png-path <path>] [--stat <output.json>]\n"
|
||||||
|
"[--fs-assets <path>] [--fs-cache <path>] [--fs-builtin <path>]\n"
|
||||||
|
"[--video-encode-path <path>]\n"
|
||||||
|
"[--png-reference-path <path>] [--frames <frames>] [--width <width>] [--height <height>] [--time-step <step>].\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
int application_main_headless(
|
||||||
|
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
|
||||||
|
Application *(*create_application)(int, char **),
|
||||||
|
int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc < 1)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
struct Args
|
||||||
|
{
|
||||||
|
std::string png_path;
|
||||||
|
std::string video_encode_path;
|
||||||
|
std::string png_reference_path;
|
||||||
|
std::string stat;
|
||||||
|
std::string assets;
|
||||||
|
std::string cache;
|
||||||
|
std::string builtin;
|
||||||
|
unsigned max_frames = UINT_MAX;
|
||||||
|
unsigned width = 1280;
|
||||||
|
unsigned height = 720;
|
||||||
|
double time_step = 0.01;
|
||||||
|
} args;
|
||||||
|
|
||||||
|
CLICallbacks cbs;
|
||||||
|
cbs.add("--frames", [&](CLIParser &parser) { args.max_frames = parser.next_uint(); });
|
||||||
|
cbs.add("--width", [&](CLIParser &parser) { args.width = parser.next_uint(); });
|
||||||
|
cbs.add("--height", [&](CLIParser &parser) { args.height = parser.next_uint(); });
|
||||||
|
cbs.add("--time-step", [&](CLIParser &parser) { args.time_step = parser.next_double(); });
|
||||||
|
cbs.add("--png-path", [&](CLIParser &parser) { args.png_path = parser.next_string(); });
|
||||||
|
cbs.add("--png-reference-path", [&](CLIParser &parser) { args.png_reference_path = parser.next_string(); });
|
||||||
|
cbs.add("--video-encode-path", [&](CLIParser &parser) { args.video_encode_path = parser.next_string(); });
|
||||||
|
cbs.add("--fs-assets", [&](CLIParser &parser) { args.assets = parser.next_string(); });
|
||||||
|
cbs.add("--fs-builtin", [&](CLIParser &parser) { args.builtin = parser.next_string(); });
|
||||||
|
cbs.add("--fs-cache", [&](CLIParser &parser) { args.cache = parser.next_string(); });
|
||||||
|
cbs.add("--stat", [&](CLIParser &parser) { args.stat = parser.next_string(); });
|
||||||
|
cbs.add("--help", [](CLIParser &parser)
|
||||||
|
{
|
||||||
|
print_help();
|
||||||
|
parser.end();
|
||||||
|
});
|
||||||
|
cbs.error_handler = [&]() { print_help(); };
|
||||||
|
int exit_code;
|
||||||
|
|
||||||
|
if (!Util::parse_cli_filtered(std::move(cbs), argc, argv, exit_code))
|
||||||
|
return exit_code;
|
||||||
|
|
||||||
|
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
|
||||||
|
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
|
||||||
|
Granite::Global::init(flags.manager_feature_flags);
|
||||||
|
|
||||||
|
if (flags.manager_feature_flags & Global::MANAGER_FEATURE_FILESYSTEM_BIT)
|
||||||
|
{
|
||||||
|
if (!args.assets.empty())
|
||||||
|
GRANITE_FILESYSTEM()->register_protocol("assets", std::make_unique<OSFilesystem>(args.assets));
|
||||||
|
if (!args.builtin.empty())
|
||||||
|
GRANITE_FILESYSTEM()->register_protocol("builtin", std::make_unique<OSFilesystem>(args.builtin));
|
||||||
|
if (!args.cache.empty())
|
||||||
|
GRANITE_FILESYSTEM()->register_protocol("cache", std::make_unique<OSFilesystem>(args.cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto app = std::unique_ptr<Application>(create_application(argc, argv));
|
||||||
|
|
||||||
|
if (app)
|
||||||
|
{
|
||||||
|
auto platform = std::make_unique<WSIPlatformHeadless>();
|
||||||
|
if (!platform->init(args.width, args.height))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
auto *p = platform.get();
|
||||||
|
|
||||||
|
if (!app->init_platform(std::move(platform)))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
p->set_max_frames(args.max_frames);
|
||||||
|
p->set_time_step(args.time_step);
|
||||||
|
p->init_headless(app.get());
|
||||||
|
|
||||||
|
// Ensure all startup work is complete.
|
||||||
|
while (app->get_wsi().get_device().query_initialization_progress(Vulkan::Device::InitializationStage::Pipelines) < 100 &&
|
||||||
|
app->poll())
|
||||||
|
{
|
||||||
|
p->begin_frame();
|
||||||
|
app->run_frame();
|
||||||
|
p->end_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.png_path.empty())
|
||||||
|
p->enable_png_readback(args.png_path);
|
||||||
|
|
||||||
|
if (!args.video_encode_path.empty())
|
||||||
|
{
|
||||||
|
#ifdef HAVE_GRANITE_FFMPEG
|
||||||
|
p->init_headless_recording(args.video_encode_path);
|
||||||
|
#else
|
||||||
|
LOGE("FFmpeg is not enabled in build.\n");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
Global::start_audio_system();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Run warm-up frame.
|
||||||
|
if (app->poll())
|
||||||
|
{
|
||||||
|
p->begin_frame();
|
||||||
|
app->run_frame();
|
||||||
|
p->end_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
p->wait_threads();
|
||||||
|
app->get_wsi().get_device().wait_idle();
|
||||||
|
app->get_wsi().get_device().timestamp_log_reset();
|
||||||
|
|
||||||
|
LOGI("=== Begin run ===\n");
|
||||||
|
|
||||||
|
auto start_time = get_current_time_nsecs();
|
||||||
|
unsigned rendered_frames = 0;
|
||||||
|
while (app->poll())
|
||||||
|
{
|
||||||
|
p->begin_frame();
|
||||||
|
app->run_frame();
|
||||||
|
p->end_frame();
|
||||||
|
if (!args.video_encode_path.empty() || !args.png_path.empty())
|
||||||
|
{
|
||||||
|
LOGI(" Queued frame %u (Total time = %.3f ms).\n", rendered_frames,
|
||||||
|
1e-6 * double(get_current_time_nsecs() - start_time));
|
||||||
|
}
|
||||||
|
rendered_frames++;
|
||||||
|
}
|
||||||
|
|
||||||
|
p->wait_threads();
|
||||||
|
app->get_wsi().get_device().wait_idle();
|
||||||
|
auto end_time = get_current_time_nsecs();
|
||||||
|
|
||||||
|
LOGI("=== End run ===\n");
|
||||||
|
|
||||||
|
struct Report
|
||||||
|
{
|
||||||
|
std::string tag;
|
||||||
|
TimestampIntervalReport report;
|
||||||
|
};
|
||||||
|
std::vector<Report> reports;
|
||||||
|
app->get_wsi().get_device().timestamp_log([&](const std::string &tag, const TimestampIntervalReport &report) {
|
||||||
|
reports.push_back({ tag, report });
|
||||||
|
});
|
||||||
|
app->get_wsi().get_device().timestamp_log_reset();
|
||||||
|
|
||||||
|
if (rendered_frames)
|
||||||
|
{
|
||||||
|
double usec = 1e-3 * double(end_time - start_time) / rendered_frames;
|
||||||
|
LOGI("Average frame time: %.3f usec\n", usec);
|
||||||
|
|
||||||
|
if (!args.stat.empty())
|
||||||
|
{
|
||||||
|
Document doc;
|
||||||
|
doc.SetObject();
|
||||||
|
auto &allocator = doc.GetAllocator();
|
||||||
|
|
||||||
|
doc.AddMember("averageFrameTimeUs", usec, allocator);
|
||||||
|
doc.AddMember("gpu", StringRef(app->get_wsi().get_context().get_gpu_props().deviceName), allocator);
|
||||||
|
doc.AddMember("driverVersion", app->get_wsi().get_context().get_gpu_props().driverVersion, allocator);
|
||||||
|
|
||||||
|
if (!reports.empty())
|
||||||
|
{
|
||||||
|
Value report_objs(kObjectType);
|
||||||
|
for (auto &rep : reports)
|
||||||
|
{
|
||||||
|
Value report_obj(kObjectType);
|
||||||
|
report_obj.AddMember("timePerAccumulationUs", 1e6 * rep.report.time_per_accumulation, allocator);
|
||||||
|
report_obj.AddMember("timePerFrameContextUs", 1e6 * rep.report.time_per_frame_context, allocator);
|
||||||
|
report_obj.AddMember("accumulationsPerFrameContext", rep.report.accumulations_per_frame_context, allocator);
|
||||||
|
report_objs.AddMember(StringRef(rep.tag), report_obj, allocator);
|
||||||
|
}
|
||||||
|
doc.AddMember("performance", report_objs, allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuffer buffer;
|
||||||
|
PrettyWriter<StringBuffer> writer(buffer);
|
||||||
|
doc.Accept(writer);
|
||||||
|
|
||||||
|
if (!GRANITE_FILESYSTEM()->write_string_to_file(args.stat, buffer.GetString()))
|
||||||
|
LOGE("Failed to write stat file to disk.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.png_reference_path.empty())
|
||||||
|
{
|
||||||
|
p->set_next_readback(args.png_reference_path);
|
||||||
|
p->begin_frame();
|
||||||
|
app->run_frame();
|
||||||
|
p->end_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
p->wait_threads();
|
||||||
|
|
||||||
|
#ifdef HAVE_GRANITE_AUDIO
|
||||||
|
Global::stop_audio_system();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
app.reset();
|
||||||
|
Granite::Global::deinit();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "application_glue.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
int application_main(
|
||||||
|
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
|
||||||
|
Application *(*create_application)(int, char **), int argc, char *argv[])
|
||||||
|
{
|
||||||
|
return application_main_headless(query_application_interface, create_application, argc, argv);
|
||||||
|
}
|
||||||
|
}
|
||||||
+441
@@ -0,0 +1,441 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "application_libretro_utils.hpp"
|
||||||
|
#include "global_managers_init.hpp"
|
||||||
|
#include "application.hpp"
|
||||||
|
#include "application_wsi.hpp"
|
||||||
|
#include "muglm/muglm_impl.hpp"
|
||||||
|
|
||||||
|
using namespace Granite;
|
||||||
|
|
||||||
|
static Application *app;
|
||||||
|
static retro_environment_t environ_cb;
|
||||||
|
static retro_video_refresh_t video_cb;
|
||||||
|
static retro_audio_sample_batch_t audio_cb;
|
||||||
|
static retro_input_poll_t input_poll_cb;
|
||||||
|
static retro_input_state_t input_state_cb;
|
||||||
|
|
||||||
|
static retro_usec_t last_frame_time;
|
||||||
|
static std::string application_name;
|
||||||
|
static std::string application_internal_resolution;
|
||||||
|
|
||||||
|
static unsigned current_width;
|
||||||
|
static unsigned current_height;
|
||||||
|
|
||||||
|
struct WSIPlatformLibretro : Granite::GraniteWSIPlatform
|
||||||
|
{
|
||||||
|
VkSurfaceKHR create_surface(VkInstance, VkPhysicalDevice) override
|
||||||
|
{
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const char *> get_instance_extensions() override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_surface_width() override
|
||||||
|
{
|
||||||
|
return current_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned get_surface_height() override
|
||||||
|
{
|
||||||
|
return current_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool alive(Vulkan::WSI &) override
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll_input() override
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||||
|
input_poll_cb();
|
||||||
|
|
||||||
|
auto &tracker = get_input_tracker();
|
||||||
|
const auto poll_key = [&](unsigned index, JoypadKey key, unsigned retro_key) {
|
||||||
|
tracker.joypad_key_state(index, key,
|
||||||
|
input_state_cb(index, RETRO_DEVICE_JOYPAD, 0, retro_key)
|
||||||
|
? JoypadKeyState::Pressed : JoypadKeyState::Released);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto poll_axis = [&](unsigned index, JoypadAxis axis, unsigned retro_index, unsigned retro_id) {
|
||||||
|
tracker.joyaxis_state(index, axis,
|
||||||
|
clamp(input_state_cb(index, RETRO_DEVICE_ANALOG,
|
||||||
|
retro_index, retro_id) * (1.0f / 0x7fff), -1.0f, 1.0f));
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto poll_axis_button = [&](unsigned index, JoypadAxis axis, unsigned retro_key) {
|
||||||
|
tracker.joyaxis_state(index, axis,
|
||||||
|
input_state_cb(index, RETRO_DEVICE_JOYPAD, 0, retro_key) ? 1.0f : 0.0f);
|
||||||
|
};
|
||||||
|
|
||||||
|
tracker.enable_joypad(0, 0, 0);
|
||||||
|
tracker.enable_joypad(1, 0, 0);
|
||||||
|
for (unsigned i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
poll_key(i, JoypadKey::Left, RETRO_DEVICE_ID_JOYPAD_LEFT);
|
||||||
|
poll_key(i, JoypadKey::Right, RETRO_DEVICE_ID_JOYPAD_RIGHT);
|
||||||
|
poll_key(i, JoypadKey::Up, RETRO_DEVICE_ID_JOYPAD_UP);
|
||||||
|
poll_key(i, JoypadKey::Down, RETRO_DEVICE_ID_JOYPAD_DOWN);
|
||||||
|
poll_key(i, JoypadKey::Select, RETRO_DEVICE_ID_JOYPAD_SELECT);
|
||||||
|
poll_key(i, JoypadKey::Start, RETRO_DEVICE_ID_JOYPAD_START);
|
||||||
|
poll_key(i, JoypadKey::LeftShoulder, RETRO_DEVICE_ID_JOYPAD_L);
|
||||||
|
poll_key(i, JoypadKey::LeftThumb, RETRO_DEVICE_ID_JOYPAD_L3);
|
||||||
|
poll_key(i, JoypadKey::RightShoulder, RETRO_DEVICE_ID_JOYPAD_R);
|
||||||
|
poll_key(i, JoypadKey::RightThumb, RETRO_DEVICE_ID_JOYPAD_R3);
|
||||||
|
poll_key(i, JoypadKey::South, RETRO_DEVICE_ID_JOYPAD_B);
|
||||||
|
poll_key(i, JoypadKey::East, RETRO_DEVICE_ID_JOYPAD_A);
|
||||||
|
poll_key(i, JoypadKey::North, RETRO_DEVICE_ID_JOYPAD_X);
|
||||||
|
poll_key(i, JoypadKey::West, RETRO_DEVICE_ID_JOYPAD_Y);
|
||||||
|
|
||||||
|
poll_axis(i, JoypadAxis::LeftX, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X);
|
||||||
|
poll_axis(i, JoypadAxis::LeftY, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y);
|
||||||
|
poll_axis(i, JoypadAxis::RightX, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X);
|
||||||
|
poll_axis(i, JoypadAxis::RightY, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y);
|
||||||
|
|
||||||
|
poll_axis_button(i, JoypadAxis::LeftTrigger, RETRO_DEVICE_ID_JOYPAD_L2);
|
||||||
|
poll_axis_button(i, JoypadAxis::RightTrigger, RETRO_DEVICE_ID_JOYPAD_R2);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.dispatch_current_state(app->get_platform().get_frame_timer().get_frame_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||||
|
get_input_tracker().dispatch_current_state(0.0, override_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_external_swapchain() override
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static retro_hw_render_callback hw_render;
|
||||||
|
|
||||||
|
RETRO_API void retro_init(void)
|
||||||
|
{
|
||||||
|
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
|
||||||
|
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
|
||||||
|
Global::init(flags.manager_feature_flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_deinit(void)
|
||||||
|
{
|
||||||
|
Global::deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_variables()
|
||||||
|
{
|
||||||
|
application_internal_resolution = application_name + "_internal_resolution";
|
||||||
|
|
||||||
|
static const retro_variable variables[] = {
|
||||||
|
{ application_internal_resolution.c_str(), "Internal resolution; 1280x720|640x360|1280x1024|1920x1080" },
|
||||||
|
{ nullptr, nullptr },
|
||||||
|
};
|
||||||
|
environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, const_cast<retro_variable *>(variables));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void query_variables()
|
||||||
|
{
|
||||||
|
retro_variable var = { application_internal_resolution.c_str(), nullptr };
|
||||||
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
||||||
|
{
|
||||||
|
unsigned new_width, new_height;
|
||||||
|
if (sscanf(var.value, "%ux%u", &new_width, &new_height) == 2)
|
||||||
|
{
|
||||||
|
current_width = new_width;
|
||||||
|
current_height = new_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_set_environment(retro_environment_t cb)
|
||||||
|
{
|
||||||
|
environ_cb = cb;
|
||||||
|
|
||||||
|
retro_log_callback log_interface;
|
||||||
|
if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log_interface))
|
||||||
|
Granite::libretro_log = log_interface.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_set_video_refresh(retro_video_refresh_t cb)
|
||||||
|
{
|
||||||
|
video_cb = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_set_audio_sample(retro_audio_sample_t)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb)
|
||||||
|
{
|
||||||
|
audio_cb = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_set_input_poll(retro_input_poll_t cb)
|
||||||
|
{
|
||||||
|
input_poll_cb = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_set_input_state(retro_input_state_t cb)
|
||||||
|
{
|
||||||
|
input_state_cb = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API unsigned retro_api_version(void)
|
||||||
|
{
|
||||||
|
return RETRO_API_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_get_system_info(struct retro_system_info *info)
|
||||||
|
{
|
||||||
|
info->block_extract = false;
|
||||||
|
info->library_name = "Sample Scene Viewer";
|
||||||
|
info->library_version = "0.0";
|
||||||
|
info->need_fullpath = true;
|
||||||
|
info->valid_extensions = "gltf|glb|scene";
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info)
|
||||||
|
{
|
||||||
|
info->timing.fps = 60.0;
|
||||||
|
info->timing.sample_rate = 44100.0;
|
||||||
|
info->geometry.aspect_ratio = float(current_width) / current_height;
|
||||||
|
info->geometry.base_height = current_width;
|
||||||
|
info->geometry.base_width = current_height;
|
||||||
|
info->geometry.max_width = current_width;
|
||||||
|
info->geometry.max_height = current_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_set_controller_port_device(unsigned, unsigned)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_reset(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static void check_variables()
|
||||||
|
{
|
||||||
|
bool updated = false;
|
||||||
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated)
|
||||||
|
{
|
||||||
|
unsigned old_width = current_width;
|
||||||
|
unsigned old_height = current_height;
|
||||||
|
query_variables();
|
||||||
|
if (old_width != current_width || old_height != current_height)
|
||||||
|
{
|
||||||
|
retro_system_av_info av_info;
|
||||||
|
retro_get_system_av_info(&av_info);
|
||||||
|
libretro_set_swapchain_size(current_width, current_height);
|
||||||
|
if (!environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &av_info))
|
||||||
|
{
|
||||||
|
current_width = old_width;
|
||||||
|
current_height = old_height;
|
||||||
|
libretro_set_swapchain_size(current_width, current_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_run(void)
|
||||||
|
{
|
||||||
|
if (!app)
|
||||||
|
{
|
||||||
|
// The application is dead, force a shutdown.
|
||||||
|
input_poll_cb();
|
||||||
|
environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
check_variables();
|
||||||
|
|
||||||
|
// Begin frame.
|
||||||
|
libretro_begin_frame(app->get_wsi(), last_frame_time);
|
||||||
|
|
||||||
|
// Run frame.
|
||||||
|
app->poll();
|
||||||
|
app->run_frame();
|
||||||
|
|
||||||
|
// Present.
|
||||||
|
libretro_end_frame(video_cb, app->get_wsi());
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API size_t retro_serialize_size(void)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API bool retro_serialize(void *, size_t)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API bool retro_unserialize(const void *, size_t)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_cheat_reset(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_cheat_set(unsigned, bool, const char *)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static void context_destroy(void)
|
||||||
|
{
|
||||||
|
libretro_context_destroy(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void context_reset(void)
|
||||||
|
{
|
||||||
|
retro_hw_render_interface_vulkan *vulkan;
|
||||||
|
if (!environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE, &vulkan))
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_ERROR, "Didn't get Vulkan HW interface.");
|
||||||
|
delete app;
|
||||||
|
app = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!libretro_context_reset(vulkan, *app))
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to reset Vulkan context.");
|
||||||
|
delete app;
|
||||||
|
app = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void frame_time_callback(retro_usec_t usecs)
|
||||||
|
{
|
||||||
|
last_frame_time = usecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API bool retro_load_game(const struct retro_game_info *info)
|
||||||
|
{
|
||||||
|
char *argv[] = {
|
||||||
|
const_cast<char *>("libretro-granite"),
|
||||||
|
const_cast<char *>(info->path),
|
||||||
|
nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
app = Granite::application_create(2, argv);
|
||||||
|
if (!app)
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to load scene: %s\n", info->path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_width = app->get_default_width();
|
||||||
|
current_height = app->get_default_height();
|
||||||
|
|
||||||
|
if (!app->init_platform(std::make_unique<WSIPlatformLibretro>()))
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to init platform.");
|
||||||
|
delete app;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
application_name = app->get_name();
|
||||||
|
libretro_set_application_info(application_name.c_str(), app->get_version());
|
||||||
|
|
||||||
|
setup_variables();
|
||||||
|
query_variables();
|
||||||
|
libretro_set_swapchain_size(current_width, current_height);
|
||||||
|
|
||||||
|
hw_render.context_destroy = context_destroy;
|
||||||
|
hw_render.context_reset = context_reset;
|
||||||
|
hw_render.context_type = RETRO_HW_CONTEXT_VULKAN;
|
||||||
|
hw_render.version_major = 1;
|
||||||
|
hw_render.version_minor = 0;
|
||||||
|
if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render))
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_ERROR, "SET_HW_RENDER failed, this core cannot run.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!libretro_load_game(environ_cb))
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_ERROR, "Failed to set up Vulkan application, this core cannot run.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
retro_frame_time_callback frame_cb = {};
|
||||||
|
frame_cb.callback = frame_time_callback;
|
||||||
|
frame_cb.reference = (1000000 + 30) / 60;
|
||||||
|
last_frame_time = frame_cb.reference;
|
||||||
|
environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, &frame_cb);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API bool retro_load_game_special(unsigned, const struct retro_game_info *, size_t)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void retro_unload_game(void)
|
||||||
|
{
|
||||||
|
libretro_unload_game();
|
||||||
|
delete app;
|
||||||
|
app = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API unsigned retro_get_region(void)
|
||||||
|
{
|
||||||
|
return RETRO_REGION_NTSC;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API void *retro_get_memory_data(unsigned)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
RETRO_API size_t retro_get_memory_size(unsigned)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
void application_dummy()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternatively, make sure this is linked in.
|
||||||
|
// Implementation is here to trick a linker to always let main() in static library work.
|
||||||
|
void application_setup_default_filesystem(const char *default_asset_directory)
|
||||||
|
{
|
||||||
|
auto *filesystem = GRANITE_FILESYSTEM();
|
||||||
|
if (filesystem)
|
||||||
|
Filesystem::setup_default_filesystem(filesystem, default_asset_directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+403
@@ -0,0 +1,403 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "application_libretro_utils.hpp"
|
||||||
|
#include "application.hpp"
|
||||||
|
#include "application_events.hpp"
|
||||||
|
#include "thread_group.hpp"
|
||||||
|
#include "asset_manager.hpp"
|
||||||
|
#include "context.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
retro_log_printf_t libretro_log;
|
||||||
|
static retro_hw_render_interface_vulkan *vulkan_interface;
|
||||||
|
static retro_hw_render_context_negotiation_interface_vulkan vulkan_negotiation;
|
||||||
|
static Vulkan::ContextHandle vulkan_context;
|
||||||
|
static Vulkan::ImageViewHandle swapchain_unorm_view;
|
||||||
|
static Vulkan::ImageHandle swapchain_image;
|
||||||
|
static retro_vulkan_image swapchain_image_info;
|
||||||
|
static bool can_dupe = false;
|
||||||
|
static std::string application_name;
|
||||||
|
static unsigned application_version;
|
||||||
|
|
||||||
|
static unsigned swapchain_width;
|
||||||
|
static unsigned swapchain_height;
|
||||||
|
static unsigned swapchain_frame_index;
|
||||||
|
static Vulkan::Semaphore acquire_semaphore;
|
||||||
|
|
||||||
|
static VkApplicationInfo vulkan_app = {
|
||||||
|
VK_STRUCTURE_TYPE_APPLICATION_INFO,
|
||||||
|
nullptr,
|
||||||
|
nullptr, 0,
|
||||||
|
"Granite",
|
||||||
|
0,
|
||||||
|
VK_API_VERSION_1_1,
|
||||||
|
};
|
||||||
|
|
||||||
|
void libretro_set_swapchain_size(unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
swapchain_width = width;
|
||||||
|
swapchain_height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void libretro_set_application_info(const char *name, unsigned version)
|
||||||
|
{
|
||||||
|
application_name = name;
|
||||||
|
application_version = version;
|
||||||
|
vulkan_app.pApplicationName = application_name.c_str();
|
||||||
|
vulkan_app.applicationVersion = application_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool libretro_create_device(
|
||||||
|
struct retro_vulkan_context *context,
|
||||||
|
VkInstance instance,
|
||||||
|
VkPhysicalDevice gpu,
|
||||||
|
VkSurfaceKHR surface,
|
||||||
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||||
|
const char **required_device_extensions,
|
||||||
|
unsigned num_required_device_extensions,
|
||||||
|
const char **, unsigned, // Deprecated.
|
||||||
|
const VkPhysicalDeviceFeatures *required_features)
|
||||||
|
{
|
||||||
|
if (!Vulkan::Context::init_loader(get_instance_proc_addr))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
vulkan_context = Util::make_handle<Vulkan::Context>();
|
||||||
|
Vulkan::Context::SystemHandles system_handles;
|
||||||
|
system_handles.filesystem = GRANITE_FILESYSTEM();
|
||||||
|
system_handles.thread_group = GRANITE_THREAD_GROUP();
|
||||||
|
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
|
||||||
|
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
|
||||||
|
vulkan_context->set_system_handles(system_handles);
|
||||||
|
vulkan_context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
|
||||||
|
if (!vulkan_context->init_device_from_instance(instance, gpu, surface, required_device_extensions, num_required_device_extensions,
|
||||||
|
required_features))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vulkan_context->release_device();
|
||||||
|
context->gpu = vulkan_context->get_gpu();
|
||||||
|
context->device = vulkan_context->get_device();
|
||||||
|
context->presentation_queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||||
|
context->presentation_queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||||
|
context->queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||||
|
context->queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VkInstance libretro_create_instance(
|
||||||
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||||
|
const VkApplicationInfo *app,
|
||||||
|
retro_vulkan_create_instance_wrapper_t create_instance_wrapper,
|
||||||
|
void *opaque)
|
||||||
|
{
|
||||||
|
if (!Vulkan::Context::init_loader(get_instance_proc_addr))
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
vulkan_context = Util::make_handle<Vulkan::Context>();
|
||||||
|
Vulkan::Context::SystemHandles system_handles;
|
||||||
|
system_handles.filesystem = GRANITE_FILESYSTEM();
|
||||||
|
system_handles.thread_group = GRANITE_THREAD_GROUP();
|
||||||
|
system_handles.asset_manager = GRANITE_ASSET_MANAGER();
|
||||||
|
system_handles.timeline_trace_file = system_handles.thread_group->get_timeline_trace_file();
|
||||||
|
|
||||||
|
vulkan_context->set_application_info(app);
|
||||||
|
vulkan_context->set_system_handles(system_handles);
|
||||||
|
vulkan_context->set_num_thread_indices(GRANITE_THREAD_GROUP()->get_num_threads() + 1);
|
||||||
|
|
||||||
|
struct Factory final : Vulkan::InstanceFactory
|
||||||
|
{
|
||||||
|
VkInstance create_instance(const VkInstanceCreateInfo *info) override
|
||||||
|
{
|
||||||
|
return wrapper(opaque, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
retro_vulkan_create_instance_wrapper_t wrapper = nullptr;
|
||||||
|
void *opaque = nullptr;
|
||||||
|
} factory;
|
||||||
|
|
||||||
|
factory.wrapper = create_instance_wrapper;
|
||||||
|
factory.opaque = opaque;
|
||||||
|
vulkan_context->set_instance_factory(&factory);
|
||||||
|
|
||||||
|
if (!vulkan_context->init_instance(nullptr, 0))
|
||||||
|
{
|
||||||
|
vulkan_context.reset();
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
vulkan_context->release_instance();
|
||||||
|
return vulkan_context->get_instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool libretro_create_device2(
|
||||||
|
struct retro_vulkan_context *context,
|
||||||
|
VkInstance instance,
|
||||||
|
VkPhysicalDevice gpu,
|
||||||
|
VkSurfaceKHR surface,
|
||||||
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||||
|
retro_vulkan_create_device_wrapper_t create_device_wrapper,
|
||||||
|
void *opaque)
|
||||||
|
{
|
||||||
|
// We are guaranteed that create_instance has been called here.
|
||||||
|
if (!vulkan_context)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Sanity check inputs.
|
||||||
|
if (vulkan_context->get_instance() != instance)
|
||||||
|
return false;
|
||||||
|
if (Vulkan::Context::get_instance_proc_addr() != get_instance_proc_addr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
struct Factory final : Vulkan::DeviceFactory
|
||||||
|
{
|
||||||
|
VkDevice create_device(VkPhysicalDevice gpu, const VkDeviceCreateInfo *info) override
|
||||||
|
{
|
||||||
|
return wrapper(gpu, opaque, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
retro_vulkan_create_device_wrapper_t wrapper = nullptr;
|
||||||
|
void *opaque = nullptr;
|
||||||
|
} factory;
|
||||||
|
|
||||||
|
factory.wrapper = create_device_wrapper;
|
||||||
|
factory.opaque = opaque;
|
||||||
|
vulkan_context->set_device_factory(&factory);
|
||||||
|
|
||||||
|
if (!vulkan_context->init_device(gpu, surface, nullptr, 0))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
vulkan_context->release_device();
|
||||||
|
context->gpu = vulkan_context->get_gpu();
|
||||||
|
context->device = vulkan_context->get_device();
|
||||||
|
context->presentation_queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||||
|
context->presentation_queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||||
|
context->queue = vulkan_context->get_queue_info().queues[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||||
|
context->queue_family_index = vulkan_context->get_queue_info().family_indices[Vulkan::QUEUE_INDEX_GRAPHICS];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void libretro_begin_frame(Vulkan::WSI &wsi, retro_usec_t frame_time)
|
||||||
|
{
|
||||||
|
// Setup the external frame.
|
||||||
|
vulkan_interface->wait_sync_index(vulkan_interface->handle);
|
||||||
|
wsi.set_external_frame(swapchain_frame_index, std::move(acquire_semaphore), double(frame_time) * 1e-6);
|
||||||
|
acquire_semaphore = {};
|
||||||
|
|
||||||
|
swapchain_frame_index ^= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void libretro_end_frame(retro_video_refresh_t video_cb, Vulkan::WSI &wsi)
|
||||||
|
{
|
||||||
|
// Present to libretro frontend.
|
||||||
|
auto signal_semaphore = wsi.get_device().request_semaphore(VK_SEMAPHORE_TYPE_BINARY);
|
||||||
|
vulkan_interface->set_signal_semaphore(vulkan_interface->handle,
|
||||||
|
signal_semaphore->get_semaphore());
|
||||||
|
signal_semaphore->signal_external();
|
||||||
|
|
||||||
|
acquire_semaphore = wsi.consume_external_release_semaphore();
|
||||||
|
if (acquire_semaphore && acquire_semaphore->get_semaphore() != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
vulkan_interface->set_image(vulkan_interface->handle,
|
||||||
|
&swapchain_image_info,
|
||||||
|
1, &acquire_semaphore->get_semaphore(),
|
||||||
|
VK_QUEUE_FAMILY_IGNORED);
|
||||||
|
|
||||||
|
// Lets us recycle the semaphore.
|
||||||
|
acquire_semaphore->wait_external();
|
||||||
|
|
||||||
|
video_cb(RETRO_HW_FRAME_BUFFER_VALID, swapchain_width, swapchain_height, 0);
|
||||||
|
can_dupe = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
vulkan_interface->set_image(vulkan_interface->handle,
|
||||||
|
&swapchain_image_info,
|
||||||
|
0, nullptr,
|
||||||
|
VK_QUEUE_FAMILY_IGNORED);
|
||||||
|
|
||||||
|
if (!can_dupe)
|
||||||
|
{
|
||||||
|
// Need something to show ... Just clear the image to black and present that.
|
||||||
|
// This should only happen if we don't render to swapchain the very first frame,
|
||||||
|
// so performance doesn't really matter.
|
||||||
|
auto &device = wsi.get_device();
|
||||||
|
auto cmd = device.request_command_buffer();
|
||||||
|
cmd->image_barrier(*swapchain_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, VK_PIPELINE_STAGE_2_CLEAR_BIT,
|
||||||
|
VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||||
|
cmd->clear_image(*swapchain_image, {});
|
||||||
|
cmd->image_barrier(*swapchain_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
|
VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL,
|
||||||
|
VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
|
||||||
|
device.submit(cmd);
|
||||||
|
video_cb(RETRO_HW_FRAME_BUFFER_VALID, swapchain_width, swapchain_height, 0);
|
||||||
|
can_dupe = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
video_cb(nullptr, swapchain_width, swapchain_height, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark video_cb has having done work in our frame context.
|
||||||
|
wsi.get_device().submit_external(Vulkan::CommandBuffer::Type::Generic);
|
||||||
|
|
||||||
|
acquire_semaphore = signal_semaphore;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool libretro_context_reset(retro_hw_render_interface_vulkan *vulkan, Granite::Application &app)
|
||||||
|
{
|
||||||
|
vulkan_interface = vulkan;
|
||||||
|
if (vulkan->interface_type != RETRO_HW_RENDER_INTERFACE_VULKAN)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (vulkan->interface_version != RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!app.init_wsi(std::move(vulkan_context)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto &device = app.get_wsi().get_device();
|
||||||
|
device.set_queue_lock([vulkan]() {
|
||||||
|
vulkan->lock_queue(vulkan->handle);
|
||||||
|
},
|
||||||
|
[vulkan]() {
|
||||||
|
vulkan->unlock_queue(vulkan->handle);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsigned num_swapchain_images = 2;
|
||||||
|
|
||||||
|
Vulkan::ImageCreateInfo info = Vulkan::ImageCreateInfo::render_target(swapchain_width, swapchain_height,
|
||||||
|
VK_FORMAT_R8G8B8A8_SRGB);
|
||||||
|
info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||||
|
info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||||
|
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
|
||||||
|
swapchain_image = device.create_image(info, nullptr);
|
||||||
|
swapchain_image->set_swapchain_layout(VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL);
|
||||||
|
can_dupe = false;
|
||||||
|
|
||||||
|
Vulkan::ImageViewCreateInfo view_info;
|
||||||
|
view_info.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
view_info.image = swapchain_image.get();
|
||||||
|
swapchain_unorm_view = device.create_image_view(view_info);
|
||||||
|
|
||||||
|
std::vector<Vulkan::ImageHandle> images;
|
||||||
|
for (unsigned i = 0; i < num_swapchain_images; i++)
|
||||||
|
images.push_back(swapchain_image);
|
||||||
|
|
||||||
|
device.init_frame_contexts(2);
|
||||||
|
if (!app.get_wsi().init_external_swapchain(std::move(images)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Setup the swapchain image info for the frontend.
|
||||||
|
swapchain_image_info.image_view = swapchain_unorm_view->get_view().view;
|
||||||
|
swapchain_image_info.image_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
|
||||||
|
swapchain_image_info.create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||||
|
swapchain_image_info.create_info.image = swapchain_unorm_view->get_image().get_image();
|
||||||
|
swapchain_image_info.create_info.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
swapchain_image_info.create_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
|
||||||
|
swapchain_image_info.create_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
|
||||||
|
swapchain_image_info.create_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
|
||||||
|
swapchain_image_info.create_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
|
||||||
|
swapchain_image_info.create_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
swapchain_image_info.create_info.subresourceRange.levelCount = 1;
|
||||||
|
swapchain_image_info.create_info.subresourceRange.layerCount = 1;
|
||||||
|
swapchain_image_info.create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
swapchain_frame_index = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void libretro_context_destroy(Granite::Application *app)
|
||||||
|
{
|
||||||
|
swapchain_unorm_view.reset();
|
||||||
|
swapchain_image.reset();
|
||||||
|
acquire_semaphore.reset();
|
||||||
|
|
||||||
|
if (app)
|
||||||
|
app->teardown_wsi();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const VkApplicationInfo *get_application_info(void)
|
||||||
|
{
|
||||||
|
return &vulkan_app;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool libretro_load_game(retro_environment_t environ_cb)
|
||||||
|
{
|
||||||
|
vulkan_negotiation.interface_type = RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN;
|
||||||
|
if (!environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT, &vulkan_negotiation))
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_WARN, "GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT failed, assuming v1 only.\n");
|
||||||
|
vulkan_negotiation.interface_version = 1;
|
||||||
|
}
|
||||||
|
else if (vulkan_negotiation.interface_version == 0)
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_ERROR, "Vulkan is not supported, this core cannot run.\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_INFO, "GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT passed, exposing v2.\n");
|
||||||
|
vulkan_negotiation.interface_version = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
vulkan_negotiation.create_device = Granite::libretro_create_device;
|
||||||
|
vulkan_negotiation.create_device2 = Granite::libretro_create_device2;
|
||||||
|
vulkan_negotiation.create_instance = Granite::libretro_create_instance;
|
||||||
|
vulkan_negotiation.destroy_device = nullptr;
|
||||||
|
vulkan_negotiation.get_application_info = get_application_info;
|
||||||
|
|
||||||
|
if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE, &vulkan_negotiation))
|
||||||
|
{
|
||||||
|
Granite::libretro_log(RETRO_LOG_ERROR, "SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE failed, this core cannot run.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
{
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void libretro_unload_game()
|
||||||
|
{
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
{
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+71
@@ -0,0 +1,71 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "vulkan_headers.hpp"
|
||||||
|
#include "libretro.h"
|
||||||
|
#include "libretro_vulkan.h"
|
||||||
|
#include "application.hpp"
|
||||||
|
|
||||||
|
// Various utilities to make writing a libretro Vulkan interface easier.
|
||||||
|
// The heavy lifting of WSI interfacing with the libretro frontend is implemented here.
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
extern retro_log_printf_t libretro_log;
|
||||||
|
|
||||||
|
bool libretro_create_device(
|
||||||
|
struct retro_vulkan_context *context,
|
||||||
|
VkInstance instance,
|
||||||
|
VkPhysicalDevice gpu,
|
||||||
|
VkSurfaceKHR surface,
|
||||||
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||||
|
const char **required_device_extensions,
|
||||||
|
unsigned num_required_device_extensions,
|
||||||
|
const char **required_device_layers,
|
||||||
|
unsigned num_required_device_layers,
|
||||||
|
const VkPhysicalDeviceFeatures *required_features);
|
||||||
|
|
||||||
|
// Takes effect next time the swapchain is recreated, on context_reset.
|
||||||
|
void libretro_set_swapchain_size(unsigned width, unsigned height);
|
||||||
|
|
||||||
|
// Used in get_application_info.
|
||||||
|
void libretro_set_application_info(const char *name, unsigned version);
|
||||||
|
|
||||||
|
// Called on context_reset HW_RENDER callback.
|
||||||
|
bool libretro_context_reset(retro_hw_render_interface_vulkan *vulkan, Granite::Application &app);
|
||||||
|
|
||||||
|
// Called on context_destroy HW_RENDER callback.
|
||||||
|
void libretro_context_destroy(Granite::Application *app);
|
||||||
|
|
||||||
|
// Called at the start of the frame.
|
||||||
|
void libretro_begin_frame(Vulkan::WSI &wsi, retro_usec_t frame_time);
|
||||||
|
|
||||||
|
// Called at the end of the frame.
|
||||||
|
void libretro_end_frame(retro_video_refresh_t video_cb, Vulkan::WSI &wsi);
|
||||||
|
|
||||||
|
// Called on retro_load_game.
|
||||||
|
bool libretro_load_game(retro_environment_t environ_cb);
|
||||||
|
// Called on retro_unload_game.
|
||||||
|
void libretro_unload_game();
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "../application_glue.hpp"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
int application_main(
|
||||||
|
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
|
||||||
|
Application *(*create_application)(int, char **), int argc, char *argv[])
|
||||||
|
{
|
||||||
|
(void)query_application_interface;
|
||||||
|
(void)create_application;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
+856
@@ -0,0 +1,856 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_vulkan.h>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#include "application.hpp"
|
||||||
|
#include "application_wsi.hpp"
|
||||||
|
#include "application_events.hpp"
|
||||||
|
#include "input.hpp"
|
||||||
|
#include "input_sdl.hpp"
|
||||||
|
#include "cli_parser.hpp"
|
||||||
|
#include "global_managers_init.hpp"
|
||||||
|
#include "timeline_trace_file.hpp"
|
||||||
|
#include "path_utils.hpp"
|
||||||
|
#include "thread_group.hpp"
|
||||||
|
#include "thread_id.hpp"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
static Key sdl_key_to_granite_key(SDL_Keycode key)
|
||||||
|
{
|
||||||
|
if (key >= 'a' && key <= 'z')
|
||||||
|
return Key(int(Granite::Key::A) + (key - 'a'));
|
||||||
|
else if (key >= 'A' && key <= 'Z')
|
||||||
|
return Key(int(Granite::Key::A) + (key - 'A'));
|
||||||
|
|
||||||
|
#define k(sdl, granite) case SDLK_##sdl: return Key::granite
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
k(LCTRL, LeftCtrl);
|
||||||
|
k(LALT, LeftAlt);
|
||||||
|
k(LSHIFT, LeftShift);
|
||||||
|
k(RETURN, Return);
|
||||||
|
k(SPACE, Space);
|
||||||
|
k(ESCAPE, Escape);
|
||||||
|
k(LEFT, Left);
|
||||||
|
k(RIGHT, Right);
|
||||||
|
k(UP, Up);
|
||||||
|
k(DOWN, Down);
|
||||||
|
k(0, _0);
|
||||||
|
k(1, _1);
|
||||||
|
k(2, _2);
|
||||||
|
k(3, _3);
|
||||||
|
k(4, _4);
|
||||||
|
k(5, _5);
|
||||||
|
k(6, _6);
|
||||||
|
k(7, _7);
|
||||||
|
k(8, _8);
|
||||||
|
k(9, _9);
|
||||||
|
default:
|
||||||
|
return Key::Unknown;
|
||||||
|
}
|
||||||
|
#undef k
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WSIPlatformSDL : GraniteWSIPlatform
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Options
|
||||||
|
{
|
||||||
|
unsigned override_width = 0;
|
||||||
|
unsigned override_height = 0;
|
||||||
|
bool fullscreen = false;
|
||||||
|
#ifdef _WIN32
|
||||||
|
bool threaded = true;
|
||||||
|
#else
|
||||||
|
bool threaded = false;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit WSIPlatformSDL(const Options &options_)
|
||||||
|
: options(options_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_gamepad_init()
|
||||||
|
{
|
||||||
|
Util::Timer tmp_timer;
|
||||||
|
tmp_timer.start();
|
||||||
|
|
||||||
|
if (!SDL_Init(SDL_INIT_GAMEPAD))
|
||||||
|
{
|
||||||
|
LOGE("Failed to init gamepad.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGI("SDL_Init(GAMEPAD) took %.3f seconds async.\n", tmp_timer.end());
|
||||||
|
|
||||||
|
LOGI("Pushing task to main thread.\n");
|
||||||
|
push_task_to_main_thread([this]() {
|
||||||
|
LOGI("Running task in main thread.\n");
|
||||||
|
if (!pad.init(get_input_tracker(), [](std::function<void ()> func) { func(); }))
|
||||||
|
LOGE("Failed to init gamepad tracker.\n");
|
||||||
|
|
||||||
|
gamepad_init_async.store(true, std::memory_order_release);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void kick_gamepad_init()
|
||||||
|
{
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||||
|
// Adding gamepad events will make main loop spin without waiting.
|
||||||
|
SDL_SetHint(SDL_HINT_AUTO_UPDATE_JOYSTICKS, "0");
|
||||||
|
|
||||||
|
// Enumerating gamepads can be extremely slow in some cases. Do this async.
|
||||||
|
// Gamepad interface is very async friendly.
|
||||||
|
|
||||||
|
gamepad_init_async = false;
|
||||||
|
|
||||||
|
if (auto *tg = GRANITE_THREAD_GROUP())
|
||||||
|
{
|
||||||
|
gamepad_init_task = tg->create_task([this]() { run_gamepad_init(); });
|
||||||
|
gamepad_init_task->set_desc("SDL init gamepad");
|
||||||
|
gamepad_init_task->set_task_class(TaskClass::Background);
|
||||||
|
gamepad_init_task->flush();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
run_gamepad_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool init(const std::string &name, unsigned width_, unsigned height_)
|
||||||
|
{
|
||||||
|
request_tear_down.store(false);
|
||||||
|
width = width_;
|
||||||
|
height = height_;
|
||||||
|
|
||||||
|
if (options.override_width)
|
||||||
|
width = options.override_width;
|
||||||
|
if (options.override_height)
|
||||||
|
height = options.override_height;
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
// RenderDoc doesn't support Wayland, and SDL3 uses Wayland by default.
|
||||||
|
// Opt in to X11 to avoid having to manually remember to pass down SDL_VIDEO_DRIVER=x11.
|
||||||
|
void *renderdoc_module = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
|
||||||
|
if (renderdoc_module)
|
||||||
|
{
|
||||||
|
LOGI("RenderDoc is loaded, disabling Wayland.\n");
|
||||||
|
setenv("SDL_VIDEO_DRIVER", "x11", 0);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Util::Timer tmp_timer;
|
||||||
|
tmp_timer.start();
|
||||||
|
if (!SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO))
|
||||||
|
{
|
||||||
|
LOGE("Failed to init SDL.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LOGI("SDL_Init took %.3f seconds.\n", tmp_timer.end());
|
||||||
|
|
||||||
|
kick_gamepad_init();
|
||||||
|
|
||||||
|
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, false);
|
||||||
|
SDL_SetEventEnabled(SDL_EVENT_DROP_TEXT, false);
|
||||||
|
|
||||||
|
if (!SDL_Vulkan_LoadLibrary(nullptr))
|
||||||
|
{
|
||||||
|
LOGE("Failed to load Vulkan library.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Vulkan::Context::init_loader(
|
||||||
|
reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr())))
|
||||||
|
{
|
||||||
|
LOGE("Failed to initialize Vulkan loader.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
wake_event_type = SDL_RegisterEvents(1);
|
||||||
|
|
||||||
|
application.name = name;
|
||||||
|
if (application.name.empty())
|
||||||
|
application.name = Path::basename(Path::get_executable_path());
|
||||||
|
|
||||||
|
window = SDL_CreateWindow(application.name.empty() ? "SDL Window" : application.name.c_str(),
|
||||||
|
int(width), int(height), SDL_WINDOW_RESIZABLE | SDL_WINDOW_VULKAN);
|
||||||
|
if (!window)
|
||||||
|
{
|
||||||
|
LOGE("Failed to create SDL window.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.fullscreen)
|
||||||
|
toggle_fullscreen();
|
||||||
|
|
||||||
|
application.info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
|
||||||
|
application.info.pEngineName = "Granite";
|
||||||
|
application.info.pApplicationName = application.name.empty() ? "Granite" : application.name.c_str();
|
||||||
|
application.info.apiVersion = VK_API_VERSION_1_1;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkApplicationInfo *get_application_info() override
|
||||||
|
{
|
||||||
|
return &application.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
void begin_drop_event() override
|
||||||
|
{
|
||||||
|
push_task_to_main_thread([]() {
|
||||||
|
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void show_message_box(const std::string &str, MessageType type) override
|
||||||
|
{
|
||||||
|
push_task_to_main_thread([this, str, type]() {
|
||||||
|
const char *title = nullptr;
|
||||||
|
Uint32 flags = 0;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case MessageType::Error:
|
||||||
|
flags = SDL_MESSAGEBOX_ERROR;
|
||||||
|
title = "Error";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MessageType::Warning:
|
||||||
|
flags = SDL_MESSAGEBOX_WARNING;
|
||||||
|
title = "Warning";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MessageType::Info:
|
||||||
|
flags = SDL_MESSAGEBOX_INFORMATION;
|
||||||
|
title = "Info";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_ShowSimpleMessageBox(flags, title, str.c_str(), window);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uintptr_t get_native_window() override
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||||
|
SDL_LockProperties(props);
|
||||||
|
auto hwnd = static_cast<HWND>(SDL_GetPointerProperty(props, "SDL.window.win32.hwnd", nullptr));
|
||||||
|
SDL_UnlockProperties(props);
|
||||||
|
return reinterpret_cast<uintptr_t>(hwnd);
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggle_fullscreen()
|
||||||
|
{
|
||||||
|
bool is_fullscreen = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||||
|
|
||||||
|
if (!is_fullscreen)
|
||||||
|
{
|
||||||
|
if (!SDL_SetWindowFullscreen(window, true))
|
||||||
|
{
|
||||||
|
LOGE("Failed to toggle fullscreen.\n");
|
||||||
|
}
|
||||||
|
#ifdef _WIN32
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||||
|
SDL_LockProperties(props);
|
||||||
|
auto hwnd = static_cast<HWND>(SDL_GetPointerProperty(props, "SDL.window.win32.hwnd", nullptr));
|
||||||
|
SDL_UnlockProperties(props);
|
||||||
|
|
||||||
|
push_task_to_async_thread([this, hwnd]() {
|
||||||
|
set_hmonitor(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
push_task_to_async_thread([this]() {
|
||||||
|
set_hmonitor(nullptr);
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
SDL_SetWindowFullscreen(window, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool alive(Vulkan::WSI &) override
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||||
|
flush_deferred_input_events();
|
||||||
|
process_events_async_thread();
|
||||||
|
process_events_async_thread_non_pollable();
|
||||||
|
return !request_tear_down.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll_input() override
|
||||||
|
{
|
||||||
|
if (!options.threaded && !iterate_message_loop())
|
||||||
|
request_tear_down = true;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||||
|
flush_deferred_input_events();
|
||||||
|
process_events_async_thread();
|
||||||
|
|
||||||
|
if (gamepad_init_async.load(std::memory_order_acquire))
|
||||||
|
pad.update(get_input_tracker());
|
||||||
|
get_input_tracker().dispatch_current_state(get_frame_timer().get_frame_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll_input_async(Granite::InputTrackerHandler *override_handler) override
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{get_input_tracker().get_lock()};
|
||||||
|
begin_async_input_handling();
|
||||||
|
{
|
||||||
|
process_events_async_thread();
|
||||||
|
if (gamepad_init_async.load(std::memory_order_acquire))
|
||||||
|
pad.update(get_input_tracker());
|
||||||
|
}
|
||||||
|
end_async_input_handling();
|
||||||
|
get_input_tracker().dispatch_current_state(0.0, override_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const char *> get_instance_extensions() override
|
||||||
|
{
|
||||||
|
uint32_t count;
|
||||||
|
const char * const *ext = SDL_Vulkan_GetInstanceExtensions(&count);
|
||||||
|
return { ext, ext + count };
|
||||||
|
}
|
||||||
|
|
||||||
|
VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice) override
|
||||||
|
{
|
||||||
|
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||||
|
if (!SDL_Vulkan_CreateSurface(window, instance, nullptr, &surface))
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
int actual_width, actual_height;
|
||||||
|
SDL_GetWindowSizeInPixels(window, &actual_width, &actual_height);
|
||||||
|
width = unsigned(actual_width);
|
||||||
|
height = unsigned(actual_height);
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t get_surface_width() override
|
||||||
|
{
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t get_surface_height() override
|
||||||
|
{
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
~WSIPlatformSDL()
|
||||||
|
{
|
||||||
|
if (gamepad_init_task)
|
||||||
|
gamepad_init_task->wait();
|
||||||
|
|
||||||
|
if (window)
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
|
||||||
|
pad.close();
|
||||||
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void block_until_wsi_forward_progress(Vulkan::WSI &wsi) override
|
||||||
|
{
|
||||||
|
if (options.threaded)
|
||||||
|
{
|
||||||
|
get_frame_timer().enter_idle();
|
||||||
|
while (!resize && alive(wsi))
|
||||||
|
process_events_async_thread_blocking();
|
||||||
|
get_frame_timer().leave_idle();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WSIPlatform::block_until_wsi_forward_progress(wsi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify_resize(unsigned width_, unsigned height_)
|
||||||
|
{
|
||||||
|
LOGI("Resize: %u x %u\n", width_, height_);
|
||||||
|
push_task_to_async_thread([=]() {
|
||||||
|
resize = true;
|
||||||
|
width = width_;
|
||||||
|
height = height_;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify_current_swapchain_dimensions(unsigned width_, unsigned height_) override
|
||||||
|
{
|
||||||
|
push_task_to_main_thread([=]() {
|
||||||
|
WSIPlatform::notify_current_swapchain_dimensions(width_, height_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_window_title(const std::string &title) override
|
||||||
|
{
|
||||||
|
push_task_to_main_thread([=]() {
|
||||||
|
if (window)
|
||||||
|
SDL_SetWindowTitle(window, title.c_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool process_sdl_event(const SDL_Event &e)
|
||||||
|
{
|
||||||
|
if (e.type == wake_event_type)
|
||||||
|
{
|
||||||
|
LOGI("Processing events main thread.\n");
|
||||||
|
process_events_main_thread();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto dispatcher = [this](std::function<void ()> func) {
|
||||||
|
push_task_to_async_thread(std::move(func));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pad.process_sdl_event(e, get_input_tracker(), dispatcher))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
switch (e.type)
|
||||||
|
{
|
||||||
|
case SDL_EVENT_QUIT:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case SDL_EVENT_WINDOW_RESIZED:
|
||||||
|
if (e.window.windowID == SDL_GetWindowID(window))
|
||||||
|
notify_resize(e.window.data1, e.window.data2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||||
|
if (e.button.windowID == SDL_GetWindowID(window))
|
||||||
|
{
|
||||||
|
MouseButton btn;
|
||||||
|
if (e.button.button == SDL_BUTTON_LEFT)
|
||||||
|
btn = MouseButton::Left;
|
||||||
|
else if (e.button.button == SDL_BUTTON_MIDDLE)
|
||||||
|
btn = MouseButton::Middle;
|
||||||
|
else if (e.button.button == SDL_BUTTON_RIGHT)
|
||||||
|
btn = MouseButton::Right;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
|
||||||
|
push_task_to_async_thread(
|
||||||
|
[this, btn, x = e.button.x, y = e.button.y,
|
||||||
|
pressed = e.type == SDL_EVENT_MOUSE_BUTTON_DOWN]() {
|
||||||
|
get_input_tracker().mouse_button_event(btn, x, y, pressed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_WINDOW_MOUSE_ENTER:
|
||||||
|
if (e.window.windowID == SDL_GetWindowID(window))
|
||||||
|
{
|
||||||
|
float x, y;
|
||||||
|
SDL_GetMouseState(&x, &y);
|
||||||
|
push_task_to_async_thread([this, x, y]() {
|
||||||
|
get_input_tracker().mouse_enter(x, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||||
|
if (e.window.windowID == SDL_GetWindowID(window))
|
||||||
|
{
|
||||||
|
push_task_to_async_thread([this]() {
|
||||||
|
get_input_tracker().mouse_leave();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_MOUSE_MOTION:
|
||||||
|
if (e.motion.windowID == SDL_GetWindowID(window))
|
||||||
|
{
|
||||||
|
push_task_to_async_thread([this, x = e.motion.x, y = e.motion.y]() {
|
||||||
|
get_input_tracker().mouse_move_event_absolute(x, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_KEY_DOWN:
|
||||||
|
case SDL_EVENT_KEY_UP:
|
||||||
|
if (e.key.windowID == SDL_GetWindowID(window))
|
||||||
|
{
|
||||||
|
KeyState state;
|
||||||
|
if (e.key.repeat)
|
||||||
|
state = KeyState::Repeat;
|
||||||
|
else if (e.type == SDL_EVENT_KEY_DOWN)
|
||||||
|
state = KeyState::Pressed;
|
||||||
|
else
|
||||||
|
state = KeyState::Released;
|
||||||
|
|
||||||
|
if (state == KeyState::Pressed && e.key.key == SDLK_ESCAPE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (state == KeyState::Pressed && e.key.key == SDLK_RETURN &&
|
||||||
|
(e.key.mod & SDL_KMOD_ALT) != 0)
|
||||||
|
{
|
||||||
|
toggle_fullscreen();
|
||||||
|
}
|
||||||
|
else if (state == KeyState::Pressed && tolower(e.key.key) == 'v' &&
|
||||||
|
(e.key.mod & SDL_KMOD_LCTRL) != 0)
|
||||||
|
{
|
||||||
|
push_non_pollable_task_to_async_thread([c = clipboard]() mutable {
|
||||||
|
if (auto *manager = GRANITE_EVENT_MANAGER())
|
||||||
|
manager->enqueue<Vulkan::ApplicationWindowTextDropEvent>(std::move(c));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Key key = sdl_key_to_granite_key(e.key.key);
|
||||||
|
push_task_to_async_thread([=]() {
|
||||||
|
get_input_tracker().key_event(key, state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_DROP_FILE:
|
||||||
|
if (e.drop.windowID == SDL_GetWindowID(window))
|
||||||
|
{
|
||||||
|
std::string str = e.drop.data;
|
||||||
|
push_non_pollable_task_to_async_thread([s = std::move(str)]() mutable {
|
||||||
|
if (auto *manager = GRANITE_EVENT_MANAGER())
|
||||||
|
manager->enqueue<Vulkan::ApplicationWindowFileDropEvent>(std::move(s));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_DROP_COMPLETE:
|
||||||
|
SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EVENT_CLIPBOARD_UPDATE:
|
||||||
|
if (SDL_HasClipboardText())
|
||||||
|
{
|
||||||
|
const char *text = SDL_GetClipboardText();
|
||||||
|
if (text)
|
||||||
|
clipboard = text;
|
||||||
|
else
|
||||||
|
clipboard.clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
clipboard.clear();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_message_loop()
|
||||||
|
{
|
||||||
|
SDL_Event e;
|
||||||
|
while (async_loop_alive && SDL_WaitEvent(&e))
|
||||||
|
if (!process_sdl_event(e))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool iterate_message_loop()
|
||||||
|
{
|
||||||
|
SDL_Event e;
|
||||||
|
while (SDL_PollEvent(&e))
|
||||||
|
if (!process_sdl_event(e))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_loop(Application *app)
|
||||||
|
{
|
||||||
|
auto ctx = Global::create_thread_context();
|
||||||
|
|
||||||
|
process_events_main_thread();
|
||||||
|
|
||||||
|
if (options.threaded)
|
||||||
|
{
|
||||||
|
async_loop_alive = true;
|
||||||
|
threaded_main_loop = std::thread(&WSIPlatformSDL::thread_main, this, app, std::move(ctx));
|
||||||
|
|
||||||
|
run_message_loop();
|
||||||
|
notify_close();
|
||||||
|
|
||||||
|
if (threaded_main_loop.joinable())
|
||||||
|
threaded_main_loop.join();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
thread_main(app, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dispatch_running_events()
|
||||||
|
{
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
{
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Running);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dispatch_stopped_events()
|
||||||
|
{
|
||||||
|
auto *em = GRANITE_EVENT_MANAGER();
|
||||||
|
if (em)
|
||||||
|
{
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Paused);
|
||||||
|
em->dequeue_all_latched(ApplicationLifecycleEvent::get_type_id());
|
||||||
|
em->enqueue_latched<ApplicationLifecycleEvent>(ApplicationLifecycle::Stopped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void thread_main(Application *app, Global::GlobalManagersHandle ctx)
|
||||||
|
{
|
||||||
|
if (options.threaded)
|
||||||
|
{
|
||||||
|
// Set this up as an alternative main thread.
|
||||||
|
ThreadGroup::set_async_main_thread();
|
||||||
|
Global::set_thread_context(*ctx);
|
||||||
|
Util::register_thread_index(0);
|
||||||
|
ctx.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("sdl-dispatch-running-events");
|
||||||
|
dispatch_running_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
{
|
||||||
|
GRANITE_SCOPED_TIMELINE_EVENT("sdl-start-audio-system");
|
||||||
|
Granite::Global::start_audio_system();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (app->poll())
|
||||||
|
app->run_frame();
|
||||||
|
Granite::Global::stop_audio_system();
|
||||||
|
}
|
||||||
|
dispatch_stopped_events();
|
||||||
|
push_task_to_main_thread([this]() { async_loop_alive = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify_close()
|
||||||
|
{
|
||||||
|
request_tear_down.store(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
void set_hmonitor(HMONITOR monitor)
|
||||||
|
{
|
||||||
|
current_hmonitor = monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
uintptr_t get_fullscreen_monitor() override
|
||||||
|
{
|
||||||
|
return reinterpret_cast<uintptr_t>(current_hmonitor);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
template <typename Op>
|
||||||
|
void push_task_to_async_thread(Op &&op)
|
||||||
|
{
|
||||||
|
push_task_to_list(task_list_async, std::forward<Op>(op));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Op>
|
||||||
|
void push_non_pollable_task_to_async_thread(Op &&op)
|
||||||
|
{
|
||||||
|
push_non_pollable_task_to_list(task_list_async, std::forward<Op>(op));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_Window *window = nullptr;
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
uint32_t wake_event_type = 0;
|
||||||
|
Options options;
|
||||||
|
std::string clipboard;
|
||||||
|
TaskGroupHandle gamepad_init_task;
|
||||||
|
std::atomic<bool> gamepad_init_async;
|
||||||
|
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
VkApplicationInfo info = {};
|
||||||
|
std::string name;
|
||||||
|
} application;
|
||||||
|
|
||||||
|
std::thread threaded_main_loop;
|
||||||
|
struct TaskList
|
||||||
|
{
|
||||||
|
std::mutex lock;
|
||||||
|
std::condition_variable cond;
|
||||||
|
std::vector<std::function<void ()>> list;
|
||||||
|
std::vector<std::function<void ()>> non_pollable_list;
|
||||||
|
} task_list_main, task_list_async;
|
||||||
|
|
||||||
|
static void process_events_for_list(TaskList &list, bool blocking)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> holder{list.lock};
|
||||||
|
|
||||||
|
if (blocking)
|
||||||
|
while (list.list.empty())
|
||||||
|
list.cond.wait(holder, [&list]() { return !list.list.empty(); });
|
||||||
|
|
||||||
|
for (auto &task : list.list)
|
||||||
|
task();
|
||||||
|
list.list.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Op>
|
||||||
|
void push_task_to_list(TaskList &list, Op &&op)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{list.lock};
|
||||||
|
list.list.emplace_back(std::forward<Op>(op));
|
||||||
|
list.cond.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Op>
|
||||||
|
void push_non_pollable_task_to_list(TaskList &list, Op &&op)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{list.lock};
|
||||||
|
list.non_pollable_list.emplace_back(std::forward<Op>(op));
|
||||||
|
list.cond.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void process_events_main_thread()
|
||||||
|
{
|
||||||
|
process_events_for_list(task_list_main, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void process_events_main_thread_blocking()
|
||||||
|
{
|
||||||
|
process_events_for_list(task_list_main, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void process_events_async_thread()
|
||||||
|
{
|
||||||
|
process_events_for_list(task_list_async, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void process_events_async_thread_non_pollable()
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> holder{task_list_async.lock};
|
||||||
|
for (auto &task : task_list_async.non_pollable_list)
|
||||||
|
task();
|
||||||
|
task_list_async.non_pollable_list.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void process_events_async_thread_blocking()
|
||||||
|
{
|
||||||
|
process_events_for_list(task_list_async, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTrackerSDL pad;
|
||||||
|
|
||||||
|
template <typename Op>
|
||||||
|
void push_task_to_main_thread(Op &&op)
|
||||||
|
{
|
||||||
|
push_task_to_list(task_list_main, std::forward<Op>(op));
|
||||||
|
SDL_Event wake_event = {};
|
||||||
|
wake_event.type = wake_event_type;
|
||||||
|
SDL_PushEvent(&wake_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic_bool request_tear_down;
|
||||||
|
bool async_loop_alive = false;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
HMONITOR current_hmonitor = nullptr;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
int application_main(
|
||||||
|
bool (*query_application_interface)(ApplicationQuery, void *, size_t),
|
||||||
|
Application *(*create_application)(int, char **), int argc, char *argv[])
|
||||||
|
{
|
||||||
|
ApplicationQueryDefaultManagerFlags flags{Global::MANAGER_FEATURE_DEFAULT_BITS};
|
||||||
|
query_application_interface(ApplicationQuery::DefaultManagerFlags, &flags, sizeof(flags));
|
||||||
|
Global::init(flags.manager_feature_flags);
|
||||||
|
|
||||||
|
WSIPlatformSDL::Options options;
|
||||||
|
int exit_code;
|
||||||
|
|
||||||
|
Util::CLICallbacks cbs;
|
||||||
|
cbs.add("--fullscreen", [&](Util::CLIParser &) { options.fullscreen = true; });
|
||||||
|
cbs.add("--width", [&](Util::CLIParser &parser) { options.override_width = parser.next_uint(); });
|
||||||
|
cbs.add("--height", [&](Util::CLIParser &parser) { options.override_height = parser.next_uint(); });
|
||||||
|
cbs.add("--thread-main-loop", [&](Util::CLIParser &) { options.threaded = true; });
|
||||||
|
cbs.add("--no-thread-main-loop", [&](Util::CLIParser &) { options.threaded = false; });
|
||||||
|
cbs.error_handler = [&]() { LOGE("Failed to parse CLI arguments for SDL.\n"); };
|
||||||
|
if (!Util::parse_cli_filtered(std::move(cbs), argc, argv, exit_code))
|
||||||
|
return exit_code;
|
||||||
|
|
||||||
|
auto app = std::unique_ptr<Application>(create_application(argc, argv));
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
if (app)
|
||||||
|
{
|
||||||
|
auto platform = std::make_unique<WSIPlatformSDL>(options);
|
||||||
|
auto *platform_handle = platform.get();
|
||||||
|
|
||||||
|
if (!platform->init(app->get_name(), app->get_default_width(), app->get_default_height()))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (!app->init_platform(std::move(platform)) || !app->init_wsi())
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
platform_handle->run_loop(app.get());
|
||||||
|
|
||||||
|
app.reset();
|
||||||
|
ret = EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret = EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Global::deinit();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3964
File diff suppressed because it is too large
Load Diff
Vendored
+494
@@ -0,0 +1,494 @@
|
|||||||
|
/* Copyright (C) 2010-2020 The RetroArch team
|
||||||
|
*
|
||||||
|
* ---------------------------------------------------------------------------------------------
|
||||||
|
* The following license statement only applies to this libretro API header (libretro_vulkan.h)
|
||||||
|
* ---------------------------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge,
|
||||||
|
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||||
|
* to deal in the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LIBRETRO_VULKAN_H__
|
||||||
|
#define LIBRETRO_VULKAN_H__
|
||||||
|
|
||||||
|
#include <libretro.h>
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
|
#define RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION 5
|
||||||
|
#define RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION 2
|
||||||
|
|
||||||
|
struct retro_vulkan_image
|
||||||
|
{
|
||||||
|
VkImageView image_view;
|
||||||
|
VkImageLayout image_layout;
|
||||||
|
VkImageViewCreateInfo create_info;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef void (*retro_vulkan_set_image_t)(void *handle,
|
||||||
|
const struct retro_vulkan_image *image,
|
||||||
|
uint32_t num_semaphores,
|
||||||
|
const VkSemaphore *semaphores,
|
||||||
|
uint32_t src_queue_family);
|
||||||
|
|
||||||
|
typedef uint32_t (*retro_vulkan_get_sync_index_t)(void *handle);
|
||||||
|
typedef uint32_t (*retro_vulkan_get_sync_index_mask_t)(void *handle);
|
||||||
|
typedef void (*retro_vulkan_set_command_buffers_t)(void *handle,
|
||||||
|
uint32_t num_cmd,
|
||||||
|
const VkCommandBuffer *cmd);
|
||||||
|
typedef void (*retro_vulkan_wait_sync_index_t)(void *handle);
|
||||||
|
typedef void (*retro_vulkan_lock_queue_t)(void *handle);
|
||||||
|
typedef void (*retro_vulkan_unlock_queue_t)(void *handle);
|
||||||
|
typedef void (*retro_vulkan_set_signal_semaphore_t)(void *handle, VkSemaphore semaphore);
|
||||||
|
|
||||||
|
typedef const VkApplicationInfo *(*retro_vulkan_get_application_info_t)(void);
|
||||||
|
|
||||||
|
struct retro_vulkan_context
|
||||||
|
{
|
||||||
|
VkPhysicalDevice gpu;
|
||||||
|
VkDevice device;
|
||||||
|
VkQueue queue;
|
||||||
|
uint32_t queue_family_index;
|
||||||
|
VkQueue presentation_queue;
|
||||||
|
uint32_t presentation_queue_family_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* This is only used in v1 of the negotiation interface.
|
||||||
|
* It is deprecated since it cannot express PDF2 features or optional extensions. */
|
||||||
|
typedef bool (*retro_vulkan_create_device_t)(
|
||||||
|
struct retro_vulkan_context *context,
|
||||||
|
VkInstance instance,
|
||||||
|
VkPhysicalDevice gpu,
|
||||||
|
VkSurfaceKHR surface,
|
||||||
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||||
|
const char **required_device_extensions,
|
||||||
|
unsigned num_required_device_extensions,
|
||||||
|
const char **required_device_layers,
|
||||||
|
unsigned num_required_device_layers,
|
||||||
|
const VkPhysicalDeviceFeatures *required_features);
|
||||||
|
|
||||||
|
typedef void (*retro_vulkan_destroy_device_t)(void);
|
||||||
|
|
||||||
|
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
|
||||||
|
typedef VkInstance (*retro_vulkan_create_instance_wrapper_t)(
|
||||||
|
void *opaque, const VkInstanceCreateInfo *create_info);
|
||||||
|
|
||||||
|
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
|
||||||
|
typedef VkInstance (*retro_vulkan_create_instance_t)(
|
||||||
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||||
|
const VkApplicationInfo *app,
|
||||||
|
retro_vulkan_create_instance_wrapper_t create_instance_wrapper,
|
||||||
|
void *opaque);
|
||||||
|
|
||||||
|
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
|
||||||
|
typedef VkDevice (*retro_vulkan_create_device_wrapper_t)(
|
||||||
|
VkPhysicalDevice gpu, void *opaque,
|
||||||
|
const VkDeviceCreateInfo *create_info);
|
||||||
|
|
||||||
|
/* v2 CONTEXT_NEGOTIATION_INTERFACE only. */
|
||||||
|
typedef bool (*retro_vulkan_create_device2_t)(
|
||||||
|
struct retro_vulkan_context *context,
|
||||||
|
VkInstance instance,
|
||||||
|
VkPhysicalDevice gpu,
|
||||||
|
VkSurfaceKHR surface,
|
||||||
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||||
|
retro_vulkan_create_device_wrapper_t create_device_wrapper,
|
||||||
|
void *opaque);
|
||||||
|
|
||||||
|
/* Note on thread safety:
|
||||||
|
* The Vulkan API is heavily designed around multi-threading, and
|
||||||
|
* the libretro interface for it should also be threading friendly.
|
||||||
|
* A core should be able to build command buffers and submit
|
||||||
|
* command buffers to the GPU from any thread.
|
||||||
|
*/
|
||||||
|
|
||||||
|
struct retro_hw_render_context_negotiation_interface_vulkan
|
||||||
|
{
|
||||||
|
/* Must be set to RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN. */
|
||||||
|
enum retro_hw_render_context_negotiation_interface_type interface_type;
|
||||||
|
/* Usually set to RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION,
|
||||||
|
* but can be lower depending on GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT. */
|
||||||
|
unsigned interface_version;
|
||||||
|
|
||||||
|
/* If non-NULL, returns a VkApplicationInfo struct that the frontend can use instead of
|
||||||
|
* its "default" application info.
|
||||||
|
* VkApplicationInfo::apiVersion also controls the target core Vulkan version for instance level functionality.
|
||||||
|
* Lifetime of the returned pointer must remain until the retro_vulkan_context is initialized.
|
||||||
|
*
|
||||||
|
* NOTE: For optimal compatibility with e.g. Android which is very slow to update its loader,
|
||||||
|
* a core version of 1.1 should be requested. Features beyond that can be requested with extensions.
|
||||||
|
* Vulkan 1.0 is only appropriate for legacy cores, but is still supported.
|
||||||
|
* A frontend is free to bump the instance creation apiVersion as necessary if the frontend requires more advanced core features.
|
||||||
|
*
|
||||||
|
* v2: This function must not be NULL, and must not return NULL.
|
||||||
|
* v1: It was not clearly defined if this function could return NULL.
|
||||||
|
* Frontends should be defensive and provide a default VkApplicationInfo
|
||||||
|
* if this function returns NULL or if this function is NULL.
|
||||||
|
*/
|
||||||
|
retro_vulkan_get_application_info_t get_application_info;
|
||||||
|
|
||||||
|
/* If non-NULL, the libretro core will choose one or more physical devices,
|
||||||
|
* create one or more logical devices and create one or more queues.
|
||||||
|
* The core must prepare a designated PhysicalDevice, Device, Queue and queue family index
|
||||||
|
* which the frontend will use for its internal operation.
|
||||||
|
*
|
||||||
|
* If gpu is not VK_NULL_HANDLE, the physical device provided to the frontend must be this PhysicalDevice if the call succeeds.
|
||||||
|
* The core is still free to use other physical devices for other purposes that are private to the core.
|
||||||
|
*
|
||||||
|
* The frontend will request certain extensions and layers for a device which is created.
|
||||||
|
* The core must ensure that the queue and queue_family_index support GRAPHICS and COMPUTE.
|
||||||
|
*
|
||||||
|
* If surface is not VK_NULL_HANDLE, the core must consider presentation when creating the queues.
|
||||||
|
* If presentation to "surface" is supported on the queue, presentation_queue must be equal to queue.
|
||||||
|
* If not, a second queue must be provided in presentation_queue and presentation_queue_index.
|
||||||
|
* If surface is not VK_NULL_HANDLE, the instance from frontend will have been created with supported for
|
||||||
|
* VK_KHR_surface extension.
|
||||||
|
*
|
||||||
|
* The core is free to set its own queue priorities.
|
||||||
|
* Device provided to frontend is owned by the frontend, but any additional device resources must be freed by core
|
||||||
|
* in destroy_device callback.
|
||||||
|
*
|
||||||
|
* If this function returns true, a PhysicalDevice, Device and Queues are initialized.
|
||||||
|
* If false, none of the above have been initialized and the frontend will attempt
|
||||||
|
* to fallback to "default" device creation, as if this function was never called.
|
||||||
|
*/
|
||||||
|
retro_vulkan_create_device_t create_device;
|
||||||
|
|
||||||
|
/* If non-NULL, this callback is called similar to context_destroy for HW_RENDER_INTERFACE.
|
||||||
|
* However, it will be called even if context_reset was not called.
|
||||||
|
* This can happen if the context never succeeds in being created.
|
||||||
|
* destroy_device will always be called before the VkInstance
|
||||||
|
* of the frontend is destroyed if create_device was called successfully so that the core has a chance of
|
||||||
|
* tearing down its own device resources.
|
||||||
|
*
|
||||||
|
* Only auxillary resources should be freed here, i.e. resources which are not part of retro_vulkan_context.
|
||||||
|
* v2: Auxillary instance resources created during create_instance can also be freed here.
|
||||||
|
*/
|
||||||
|
retro_vulkan_destroy_device_t destroy_device;
|
||||||
|
|
||||||
|
/* v2 API: If interface_version is < 2, fields below must be ignored.
|
||||||
|
* If the frontend does not support interface version 2, the v1 entry points will be used instead. */
|
||||||
|
|
||||||
|
/* If non-NULL, this is called to create an instance, otherwise a VkInstance is created by the frontend.
|
||||||
|
* v1 interface bug: The only way to enable instance features is through core versions signalled in VkApplicationInfo.
|
||||||
|
* The frontend may request that certain extensions and layers
|
||||||
|
* are enabled on the VkInstance. Application may add additional features.
|
||||||
|
* If app is non-NULL, apiVersion controls the minimum core version required by the application.
|
||||||
|
* Return a VkInstance or VK_NULL_HANDLE. The VkInstance is owned by the frontend.
|
||||||
|
*
|
||||||
|
* Rather than call vkCreateInstance directly, a core must call the CreateInstance wrapper provided with:
|
||||||
|
* VkInstance instance = create_instance_wrapper(opaque, &create_info);
|
||||||
|
* If the core wishes to create a private instance for whatever reason (relying on shared memory for example),
|
||||||
|
* it may call vkCreateInstance directly. */
|
||||||
|
retro_vulkan_create_instance_t create_instance;
|
||||||
|
|
||||||
|
/* If non-NULL and frontend recognizes negotiation interface >= 2, create_device2 takes precedence over create_device.
|
||||||
|
* Similar to create_device, but is extended to better understand new core versions and PDF2 feature enablement.
|
||||||
|
* Requirements for create_device2 are the same as create_device unless a difference is mentioned.
|
||||||
|
*
|
||||||
|
* v2 consideration:
|
||||||
|
* If the chosen gpu by frontend cannot be supported, a core must return false.
|
||||||
|
*
|
||||||
|
* NOTE: "Cannot be supported" is intentionally vaguely defined.
|
||||||
|
* Refusing to run on an iGPU for a very intensive core with desktop GPU as a minimum spec may be in the gray area.
|
||||||
|
* Not supporting optional features is not a good reason to reject a physical device, however.
|
||||||
|
*
|
||||||
|
* On device creation feature with explicit gpu, a frontend should fall back create_device2 with gpu == VK_NULL_HANDLE and let core
|
||||||
|
* decide on a supported device if possible.
|
||||||
|
*
|
||||||
|
* A core must assume that the explicitly provided GPU is the only guaranteed attempt it has to create a device.
|
||||||
|
* A fallback may not be attempted if there are particular reasons why only a specific physical device can work,
|
||||||
|
* but these situations should be esoteric and rare in nature, e.g. a libretro frontend is implemented with external memory
|
||||||
|
* and only LUID matching would work.
|
||||||
|
* Cores and frontends should ensure "best effort" when negotiating like this and appropriate logging is encouraged.
|
||||||
|
*
|
||||||
|
* v1 note: In the v1 version of create_device, it was never expected that create_device would fail like this,
|
||||||
|
* and frontends are not expected to attempt fall backs.
|
||||||
|
*
|
||||||
|
* Rather than call vkCreateDevice directly, a core must call the CreateDevice wrapper provided with:
|
||||||
|
* VkDevice device = create_device_wrapper(gpu, opaque, &create_info);
|
||||||
|
* If the core wishes to create a private device for whatever reason (relying on shared memory for example),
|
||||||
|
* it may call vkCreateDevice directly.
|
||||||
|
*
|
||||||
|
* This allows the frontend to add additional extensions that it requires as well as adjust the PDF2 pNext as required.
|
||||||
|
* It is also possible adjust the queue create infos in case the frontend desires to allocate some private queues.
|
||||||
|
*
|
||||||
|
* The get_instance_proc_addr provided in create_device2 must be the same as create_instance.
|
||||||
|
*
|
||||||
|
* NOTE: The frontend must not disable features requested by application.
|
||||||
|
* NOTE: The frontend must not add any robustness features as some API behavior may change (VK_EXT_descriptor_buffer comes to mind).
|
||||||
|
* I.e. robustBufferAccess and the like. (nullDescriptor from robustness2 is allowed to be enabled).
|
||||||
|
*/
|
||||||
|
retro_vulkan_create_device2_t create_device2;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct retro_hw_render_interface_vulkan
|
||||||
|
{
|
||||||
|
/* Must be set to RETRO_HW_RENDER_INTERFACE_VULKAN. */
|
||||||
|
enum retro_hw_render_interface_type interface_type;
|
||||||
|
/* Must be set to RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION. */
|
||||||
|
unsigned interface_version;
|
||||||
|
|
||||||
|
/* Opaque handle to the Vulkan backend in the frontend
|
||||||
|
* which must be passed along to all function pointers
|
||||||
|
* in this interface.
|
||||||
|
*
|
||||||
|
* The rationale for including a handle here (which libretro v1
|
||||||
|
* doesn't currently do in general) is:
|
||||||
|
*
|
||||||
|
* - Vulkan cores should be able to be freely threaded without lots of fuzz.
|
||||||
|
* This would break frontends which currently rely on TLS
|
||||||
|
* to deal with multiple cores loaded at the same time.
|
||||||
|
* - Fixing this in general is TODO for an eventual libretro v2.
|
||||||
|
*/
|
||||||
|
void *handle;
|
||||||
|
|
||||||
|
/* The Vulkan instance the context is using. */
|
||||||
|
VkInstance instance;
|
||||||
|
/* The physical device used. */
|
||||||
|
VkPhysicalDevice gpu;
|
||||||
|
/* The logical device used. */
|
||||||
|
VkDevice device;
|
||||||
|
|
||||||
|
/* Allows a core to fetch all its needed symbols without having to link
|
||||||
|
* against the loader itself. */
|
||||||
|
PFN_vkGetDeviceProcAddr get_device_proc_addr;
|
||||||
|
PFN_vkGetInstanceProcAddr get_instance_proc_addr;
|
||||||
|
|
||||||
|
/* The queue the core must use to submit data.
|
||||||
|
* This queue and index must remain constant throughout the lifetime
|
||||||
|
* of the context.
|
||||||
|
*
|
||||||
|
* This queue will be the queue that supports graphics and compute
|
||||||
|
* if the device supports compute.
|
||||||
|
*/
|
||||||
|
VkQueue queue;
|
||||||
|
unsigned queue_index;
|
||||||
|
|
||||||
|
/* Before calling retro_video_refresh_t with RETRO_HW_FRAME_BUFFER_VALID,
|
||||||
|
* set which image to use for this frame.
|
||||||
|
*
|
||||||
|
* If num_semaphores is non-zero, the frontend will wait for the
|
||||||
|
* semaphores provided to be signaled before using the results further
|
||||||
|
* in the pipeline.
|
||||||
|
*
|
||||||
|
* Semaphores provided by a single call to set_image will only be
|
||||||
|
* waited for once (waiting for a semaphore resets it).
|
||||||
|
* E.g. set_image, video_refresh, and then another
|
||||||
|
* video_refresh without set_image,
|
||||||
|
* but same image will only wait for semaphores once.
|
||||||
|
*
|
||||||
|
* For this reason, ownership transfer will only occur if semaphores
|
||||||
|
* are waited on for a particular frame in the frontend.
|
||||||
|
*
|
||||||
|
* Using semaphores is optional for synchronization purposes,
|
||||||
|
* but if not using
|
||||||
|
* semaphores, an image memory barrier in vkCmdPipelineBarrier
|
||||||
|
* should be used in the graphics_queue.
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* vkCmdPipelineBarrier(cmd,
|
||||||
|
* srcStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
|
||||||
|
* dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||||
|
* image_memory_barrier = {
|
||||||
|
* srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||||
|
* dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* The use of pipeline barriers instead of semaphores is encouraged
|
||||||
|
* as it is simpler and more fine-grained. A layout transition
|
||||||
|
* must generally happen anyways which requires a
|
||||||
|
* pipeline barrier.
|
||||||
|
*
|
||||||
|
* The image passed to set_image must have imageUsage flags set to at least
|
||||||
|
* VK_IMAGE_USAGE_TRANSFER_SRC_BIT and VK_IMAGE_USAGE_SAMPLED_BIT.
|
||||||
|
* The core will naturally want to use flags such as
|
||||||
|
* VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT and/or
|
||||||
|
* VK_IMAGE_USAGE_TRANSFER_DST_BIT depending
|
||||||
|
* on how the final image is created.
|
||||||
|
*
|
||||||
|
* The image must also have been created with MUTABLE_FORMAT bit set if
|
||||||
|
* 8-bit formats are used, so that the frontend can reinterpret sRGB
|
||||||
|
* formats as it sees fit.
|
||||||
|
*
|
||||||
|
* Images passed to set_image should be created with TILING_OPTIMAL.
|
||||||
|
* The image layout should be transitioned to either
|
||||||
|
* VK_IMAGE_LAYOUT_GENERIC or VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
|
||||||
|
* The actual image layout used must be set in image_layout.
|
||||||
|
*
|
||||||
|
* The image must be a 2D texture which may or not be layered
|
||||||
|
* and/or mipmapped.
|
||||||
|
*
|
||||||
|
* The image must be suitable for linear sampling.
|
||||||
|
* While the image_view is typically the only field used,
|
||||||
|
* the frontend may want to reinterpret the texture as sRGB vs.
|
||||||
|
* non-sRGB for example so the VkImageViewCreateInfo used to
|
||||||
|
* create the image view must also be passed in.
|
||||||
|
*
|
||||||
|
* The data in the pointer to the image struct will not be copied
|
||||||
|
* as the pNext field in create_info cannot be reliably deep-copied.
|
||||||
|
* The image pointer passed to set_image must be valid until
|
||||||
|
* retro_video_refresh_t has returned.
|
||||||
|
*
|
||||||
|
* If frame duping is used when passing NULL to retro_video_refresh_t,
|
||||||
|
* the frontend is free to either use the latest image passed to
|
||||||
|
* set_image or reuse the older pointer passed to set_image the
|
||||||
|
* frame RETRO_HW_FRAME_BUFFER_VALID was last used.
|
||||||
|
*
|
||||||
|
* Essentially, the lifetime of the pointer passed to
|
||||||
|
* retro_video_refresh_t should be extended if frame duping is used
|
||||||
|
* so that the frontend can reuse the older pointer.
|
||||||
|
*
|
||||||
|
* The image itself however, must not be touched by the core until
|
||||||
|
* wait_sync_index has been completed later. The frontend may perform
|
||||||
|
* layout transitions on the image, so even read-only access is not defined.
|
||||||
|
* The exception to read-only rule is if GENERAL layout is used for the image.
|
||||||
|
* In this case, the frontend is not allowed to perform any layout transitions,
|
||||||
|
* so concurrent reads from core and frontend are allowed.
|
||||||
|
*
|
||||||
|
* If frame duping is used, or if set_command_buffers is used,
|
||||||
|
* the frontend will not wait for any semaphores.
|
||||||
|
*
|
||||||
|
* The src_queue_family is used to specify which queue family
|
||||||
|
* the image is currently owned by. If using multiple queue families
|
||||||
|
* (e.g. async compute), the frontend will need to acquire ownership of the
|
||||||
|
* image before rendering with it and release the image afterwards.
|
||||||
|
*
|
||||||
|
* If src_queue_family is equal to the queue family (queue_index),
|
||||||
|
* no ownership transfer will occur.
|
||||||
|
* Similarly, if src_queue_family is VK_QUEUE_FAMILY_IGNORED,
|
||||||
|
* no ownership transfer will occur.
|
||||||
|
*
|
||||||
|
* The frontend will always release ownership back to src_queue_family.
|
||||||
|
* Waiting for frontend to complete with wait_sync_index() ensures that
|
||||||
|
* the frontend has released ownership back to the application.
|
||||||
|
* Note that in Vulkan, transfering ownership is a two-part process.
|
||||||
|
*
|
||||||
|
* Example frame:
|
||||||
|
* - core releases ownership from src_queue_index to queue_index with VkImageMemoryBarrier.
|
||||||
|
* - core calls set_image with src_queue_index.
|
||||||
|
* - Frontend will acquire the image with src_queue_index -> queue_index as well, completing the ownership transfer.
|
||||||
|
* - Frontend renders the frame.
|
||||||
|
* - Frontend releases ownership with queue_index -> src_queue_index.
|
||||||
|
* - Next time image is used, core must acquire ownership from queue_index ...
|
||||||
|
*
|
||||||
|
* Since the frontend releases ownership, we cannot necessarily dupe the frame because
|
||||||
|
* the core needs to make the roundtrip of ownership transfer.
|
||||||
|
*/
|
||||||
|
retro_vulkan_set_image_t set_image;
|
||||||
|
|
||||||
|
/* Get the current sync index for this frame which is obtained in
|
||||||
|
* frontend by calling e.g. vkAcquireNextImageKHR before calling
|
||||||
|
* retro_run().
|
||||||
|
*
|
||||||
|
* This index will correspond to which swapchain buffer is currently
|
||||||
|
* the active one.
|
||||||
|
*
|
||||||
|
* Knowing this index is very useful for maintaining safe asynchronous CPU
|
||||||
|
* and GPU operation without stalling.
|
||||||
|
*
|
||||||
|
* The common pattern for synchronization is to receive fences when
|
||||||
|
* submitting command buffers to Vulkan (vkQueueSubmit) and add this fence
|
||||||
|
* to a list of fences for frame number get_sync_index().
|
||||||
|
*
|
||||||
|
* Next time we receive the same get_sync_index(), we can wait for the
|
||||||
|
* fences from before, which will usually return immediately as the
|
||||||
|
* frontend will generally also avoid letting the GPU run ahead too much.
|
||||||
|
*
|
||||||
|
* After the fence has signaled, we know that the GPU has completed all
|
||||||
|
* GPU work related to work submitted in the frame we last saw get_sync_index().
|
||||||
|
*
|
||||||
|
* This means we can safely reuse or free resources allocated in this frame.
|
||||||
|
*
|
||||||
|
* In theory, even if we wait for the fences correctly, it is not technically
|
||||||
|
* safe to write to the image we earlier passed to the frontend since we're
|
||||||
|
* not waiting for the frontend GPU jobs to complete.
|
||||||
|
*
|
||||||
|
* The frontend will guarantee that the appropriate pipeline barrier
|
||||||
|
* in graphics_queue has been used such that
|
||||||
|
* VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT cannot
|
||||||
|
* start until the frontend is done with the image.
|
||||||
|
*/
|
||||||
|
retro_vulkan_get_sync_index_t get_sync_index;
|
||||||
|
|
||||||
|
/* Returns a bitmask of how many swapchain images we currently have
|
||||||
|
* in the frontend.
|
||||||
|
*
|
||||||
|
* If bit #N is set in the return value, get_sync_index can return N.
|
||||||
|
* Knowing this value is useful for preallocating per-frame management
|
||||||
|
* structures ahead of time.
|
||||||
|
*
|
||||||
|
* While this value will typically remain constant throughout the
|
||||||
|
* applications lifecycle, it may for example change if the frontend
|
||||||
|
* suddently changes fullscreen state and/or latency.
|
||||||
|
*
|
||||||
|
* If this value ever changes, it is safe to assume that the device
|
||||||
|
* is completely idle and all synchronization objects can be deleted
|
||||||
|
* right away as desired.
|
||||||
|
*/
|
||||||
|
retro_vulkan_get_sync_index_mask_t get_sync_index_mask;
|
||||||
|
|
||||||
|
/* Instead of submitting the command buffer to the queue first, the core
|
||||||
|
* can pass along its command buffer to the frontend, and the frontend
|
||||||
|
* will submit the command buffer together with the frontends command buffers.
|
||||||
|
*
|
||||||
|
* This has the advantage that the overhead of vkQueueSubmit can be
|
||||||
|
* amortized into a single call. For this mode, semaphores in set_image
|
||||||
|
* will be ignored, so vkCmdPipelineBarrier must be used to synchronize
|
||||||
|
* the core and frontend.
|
||||||
|
*
|
||||||
|
* The command buffers in set_command_buffers are only executed once,
|
||||||
|
* even if frame duping is used.
|
||||||
|
*
|
||||||
|
* If frame duping is used, set_image should be used for the frames
|
||||||
|
* which should be duped instead.
|
||||||
|
*
|
||||||
|
* Command buffers passed to the frontend with set_command_buffers
|
||||||
|
* must not actually be submitted to the GPU until retro_video_refresh_t
|
||||||
|
* is called.
|
||||||
|
*
|
||||||
|
* The frontend must submit the command buffer before submitting any
|
||||||
|
* other command buffers provided by set_command_buffers. */
|
||||||
|
retro_vulkan_set_command_buffers_t set_command_buffers;
|
||||||
|
|
||||||
|
/* Waits on CPU for device activity for the current sync index to complete.
|
||||||
|
* This is useful since the core will not have a relevant fence to sync with
|
||||||
|
* when the frontend is submitting the command buffers. */
|
||||||
|
retro_vulkan_wait_sync_index_t wait_sync_index;
|
||||||
|
|
||||||
|
/* If the core submits command buffers itself to any of the queues provided
|
||||||
|
* in this interface, the core must lock and unlock the frontend from
|
||||||
|
* racing on the VkQueue.
|
||||||
|
*
|
||||||
|
* Queue submission can happen on any thread.
|
||||||
|
* Even if queue submission happens on the same thread as retro_run(),
|
||||||
|
* the lock/unlock functions must still be called.
|
||||||
|
*
|
||||||
|
* NOTE: Queue submissions are heavy-weight. */
|
||||||
|
retro_vulkan_lock_queue_t lock_queue;
|
||||||
|
retro_vulkan_unlock_queue_t unlock_queue;
|
||||||
|
|
||||||
|
/* Sets a semaphore which is signaled when the image in set_image can safely be reused.
|
||||||
|
* The semaphore is consumed next call to retro_video_refresh_t.
|
||||||
|
* The semaphore will be signalled even for duped frames.
|
||||||
|
* The semaphore will be signalled only once, so set_signal_semaphore should be called every frame.
|
||||||
|
* The semaphore may be VK_NULL_HANDLE, which disables semaphore signalling for next call to retro_video_refresh_t.
|
||||||
|
*
|
||||||
|
* This is mostly useful to support use cases where you're rendering to a single image that
|
||||||
|
* is recycled in a ping-pong fashion with the frontend to save memory (but potentially less throughput).
|
||||||
|
*/
|
||||||
|
retro_vulkan_set_signal_semaphore_t set_signal_semaphore;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
+1618
File diff suppressed because it is too large
Load Diff
+180
@@ -0,0 +1,180 @@
|
|||||||
|
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "application.hpp"
|
||||||
|
#include "render_context.hpp"
|
||||||
|
#include "scene_loader.hpp"
|
||||||
|
#include "animation_system.hpp"
|
||||||
|
#include "renderer.hpp"
|
||||||
|
#include "timer.hpp"
|
||||||
|
#include "event.hpp"
|
||||||
|
#include "font.hpp"
|
||||||
|
#include "ui_manager.hpp"
|
||||||
|
#include "render_graph.hpp"
|
||||||
|
#include "mesh_util.hpp"
|
||||||
|
#include "scene_renderer.hpp"
|
||||||
|
#include "lights/clusterer.hpp"
|
||||||
|
#include "lights/volumetric_fog.hpp"
|
||||||
|
#include "lights/volumetric_diffuse.hpp"
|
||||||
|
#include "camera_export.hpp"
|
||||||
|
#include "post/aa.hpp"
|
||||||
|
#include "post/temporal.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class TaskComposer;
|
||||||
|
|
||||||
|
class SceneViewerApplication : public Application, public EventHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct CLIConfig
|
||||||
|
{
|
||||||
|
bool timestamp = false;
|
||||||
|
bool ocean = false;
|
||||||
|
int camera_index = -1;
|
||||||
|
};
|
||||||
|
SceneViewerApplication(const std::string &path,
|
||||||
|
const std::string &config_path, const std::string &quirks_path,
|
||||||
|
const CLIConfig &cli_config);
|
||||||
|
~SceneViewerApplication();
|
||||||
|
void render_frame(double frame_time, double elapsed_time) override;
|
||||||
|
void rescale_scene(float radius);
|
||||||
|
void loop_animations();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void update_scene(TaskComposer &composer, double frame_time, double elapsed_time);
|
||||||
|
void render_scene(TaskComposer &composer);
|
||||||
|
void post_frame() override;
|
||||||
|
|
||||||
|
RenderContext context;
|
||||||
|
RenderContext depth_contexts[NumShadowCascades];
|
||||||
|
RenderContext fallback_depth_context;
|
||||||
|
|
||||||
|
RendererSuite renderer_suite;
|
||||||
|
RendererSuite::Config renderer_suite_config;
|
||||||
|
FlatRenderer flat_renderer;
|
||||||
|
LightingParameters lighting;
|
||||||
|
LightingParameters fallback_lighting;
|
||||||
|
FPSCamera cam;
|
||||||
|
SceneLoader scene_loader;
|
||||||
|
std::unique_ptr<AnimationSystem> animation_system;
|
||||||
|
|
||||||
|
Camera *selected_camera = nullptr;
|
||||||
|
DirectionalLightComponent *selected_directional = nullptr;
|
||||||
|
DirectionalLightComponent default_directional_light;
|
||||||
|
|
||||||
|
SceneTransformManager scene_transform_manager;
|
||||||
|
|
||||||
|
void on_device_created(const Vulkan::DeviceCreatedEvent &e);
|
||||||
|
void on_device_destroyed(const Vulkan::DeviceCreatedEvent &e);
|
||||||
|
void on_swapchain_changed(const Vulkan::SwapchainParameterEvent &e);
|
||||||
|
void on_swapchain_destroyed(const Vulkan::SwapchainParameterEvent &e);
|
||||||
|
bool on_key_down(const KeyboardEvent &e);
|
||||||
|
RenderGraph graph;
|
||||||
|
|
||||||
|
bool need_shadow_map_update = true;
|
||||||
|
AABB shadow_scene_aabb;
|
||||||
|
|
||||||
|
std::unique_ptr<LightClusterer> cluster;
|
||||||
|
std::unique_ptr<VolumetricFog> volumetric_fog;
|
||||||
|
std::unique_ptr<VolumetricDiffuseLightManager> volumetric_diffuse;
|
||||||
|
RenderQueue queue;
|
||||||
|
|
||||||
|
void setup_shadow_map();
|
||||||
|
void update_shadow_scene_aabb();
|
||||||
|
void render_ui(Vulkan::CommandBuffer &cmd);
|
||||||
|
|
||||||
|
void add_main_pass(Vulkan::Device &device, const std::string &tag);
|
||||||
|
void add_main_pass_forward(Vulkan::Device &device, const std::string &tag);
|
||||||
|
void add_main_pass_deferred(Vulkan::Device &device, const std::string &tag);
|
||||||
|
void add_mv_pass(const std::string &tag, const std::string &depth, bool full_mv);
|
||||||
|
|
||||||
|
void add_shadow_pass(Vulkan::Device &device, const std::string &tag);
|
||||||
|
void add_shadow_pass_fallback(Vulkan::Device &device, const std::string &tag);
|
||||||
|
|
||||||
|
std::vector<RecordedCamera> recorded_cameras;
|
||||||
|
|
||||||
|
std::string get_name() override;
|
||||||
|
|
||||||
|
void bake_render_graph(const Vulkan::SwapchainParameterEvent &swap);
|
||||||
|
const Vulkan::SwapchainParameterEvent *pending_swapchain = nullptr;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void read_config(const std::string &path);
|
||||||
|
void read_quirks(const std::string &path);
|
||||||
|
void read_lights();
|
||||||
|
|
||||||
|
struct Config
|
||||||
|
{
|
||||||
|
RendererType renderer_type = RendererType::GeneralDeferred;
|
||||||
|
unsigned msaa = 1;
|
||||||
|
float shadow_map_resolution = 2048.0f;
|
||||||
|
unsigned clustered_lights_shadow_resolution = 512;
|
||||||
|
|
||||||
|
SceneRendererFlags pcf_flags = SCENE_RENDERER_SHADOW_PCF_WIDE_BIT;
|
||||||
|
float resolution_scale = 1.0f;
|
||||||
|
bool resolution_scale_sharpen = true;
|
||||||
|
float lod_bias = 0.0f;
|
||||||
|
bool directional_light_shadows = true;
|
||||||
|
bool directional_light_cascaded_shadows = true;
|
||||||
|
bool directional_light_shadows_vsm = false;
|
||||||
|
bool clustered_lights_shadows = true;
|
||||||
|
bool clustered_lights_shadows_vsm = false;
|
||||||
|
bool hdr_bloom = true;
|
||||||
|
bool hdr_bloom_dynamic_exposure = true;
|
||||||
|
bool forward_depth_prepass = true;
|
||||||
|
bool rt_fp16 = false;
|
||||||
|
bool rescale_scene = false;
|
||||||
|
bool show_ui = true;
|
||||||
|
bool volumetric_fog = false;
|
||||||
|
bool volumetric_fog_regions = true;
|
||||||
|
bool volumetric_diffuse = false;
|
||||||
|
bool ssao = true;
|
||||||
|
bool debug_probes = false;
|
||||||
|
bool ssr = false;
|
||||||
|
PostAAType postaa_type = PostAAType::None;
|
||||||
|
};
|
||||||
|
Config config;
|
||||||
|
CLIConfig cli_config;
|
||||||
|
|
||||||
|
void export_lights();
|
||||||
|
void export_cameras();
|
||||||
|
|
||||||
|
enum { FrameWindowSize = 64, FrameWindowSizeMask = FrameWindowSize - 1 };
|
||||||
|
float last_frame_times[FrameWindowSize] = {};
|
||||||
|
unsigned last_frame_index = 0;
|
||||||
|
|
||||||
|
TemporalJitter jitter;
|
||||||
|
void capture_environment_probe();
|
||||||
|
|
||||||
|
RenderTextureResource *ssao_output = nullptr;
|
||||||
|
RenderTextureResource *shadows = nullptr;
|
||||||
|
RenderTextureResource *fallback_shadows = nullptr;
|
||||||
|
RenderTextureResource *hiz_main = nullptr;
|
||||||
|
RenderTextureResource *hiz_depth = nullptr;
|
||||||
|
Util::SmallVector<Vulkan::ImageViewHandle> hiz_depth_peel;
|
||||||
|
|
||||||
|
CullingPassesInfo culling_passes_info = {};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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<std::string> *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<uint32_t> GLSLCompiler::compile(std::string &error_message, const std::vector<std::pair<std::string, int>> *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<uint32_t> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <stdint.h>
|
||||||
|
#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<std::string> *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<uint32_t> compile(std::string &error_message, const std::vector<std::pair<std::string, int>> *defines = nullptr) const;
|
||||||
|
|
||||||
|
const std::unordered_set<std::string> &get_dependencies() const
|
||||||
|
{
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Optimization
|
||||||
|
{
|
||||||
|
ForceOff,
|
||||||
|
ForceOn,
|
||||||
|
Default
|
||||||
|
};
|
||||||
|
|
||||||
|
void set_optimization(Optimization opt)
|
||||||
|
{
|
||||||
|
optimization = opt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_strip(bool strip_)
|
||||||
|
{
|
||||||
|
strip = strip_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string> &get_user_pragmas() const
|
||||||
|
{
|
||||||
|
return pragmas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
FilesystemInterface &iface;
|
||||||
|
std::string source;
|
||||||
|
std::string source_path;
|
||||||
|
const std::vector<std::string> *include_directories = nullptr;
|
||||||
|
Stage stage = Stage::Unknown;
|
||||||
|
|
||||||
|
std::unordered_set<std::string> dependencies;
|
||||||
|
struct Section
|
||||||
|
{
|
||||||
|
Stage stage;
|
||||||
|
std::string source;
|
||||||
|
};
|
||||||
|
Util::SmallVector<Section> preprocessed_sections;
|
||||||
|
std::string preprocessed_source;
|
||||||
|
Stage preprocessing_active_stage = Stage::Unknown;
|
||||||
|
|
||||||
|
std::vector<std::pair<size_t, size_t>> preprocessed_lines;
|
||||||
|
std::vector<std::string> pragmas;
|
||||||
|
|
||||||
|
Target target = Target::Vulkan11;
|
||||||
|
|
||||||
|
bool parse_variants(const std::string &source, const std::string &path);
|
||||||
|
|
||||||
|
Optimization optimization = Optimization::Default;
|
||||||
|
bool strip = false;
|
||||||
|
|
||||||
|
bool find_include_path(const std::string &source_path, const std::string &include_path,
|
||||||
|
std::string &included_path, std::string &included_source);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
add_granite_internal_lib(granite-ecs ecs.hpp ecs.cpp)
|
||||||
|
target_include_directories(granite-ecs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(granite-ecs PUBLIC granite-util)
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/* 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 "ecs.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
Entity *EntityPool::create_entity()
|
||||||
|
{
|
||||||
|
Util::Hasher hasher;
|
||||||
|
hasher.u64(++cookie);
|
||||||
|
auto *entity = entity_pool.allocate(this, hasher.get());
|
||||||
|
entity->pool_offset = entities.size();
|
||||||
|
entities.push_back(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityPool::free_component(Entity &entity, ComponentType id, ComponentNode *component)
|
||||||
|
{
|
||||||
|
auto *c = component_types.find(id);
|
||||||
|
assert(c);
|
||||||
|
c->free_component(component->get());
|
||||||
|
component_nodes.free(component);
|
||||||
|
|
||||||
|
auto *component_groups = component_to_groups.find(id);
|
||||||
|
if (component_groups)
|
||||||
|
{
|
||||||
|
for (auto &group : *component_groups)
|
||||||
|
{
|
||||||
|
auto *g = groups.find(group.get_hash());
|
||||||
|
if (g)
|
||||||
|
g->remove_entity(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityPool::delete_entity(Entity *entity)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto &components = entity->get_components();
|
||||||
|
auto &list = components.inner_list();
|
||||||
|
auto itr = list.begin();
|
||||||
|
while (itr != list.end())
|
||||||
|
{
|
||||||
|
auto *component = itr.get();
|
||||||
|
itr = list.erase(itr);
|
||||||
|
free_component(*entity, component->get_hash(), component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto offset = entity->pool_offset;
|
||||||
|
assert(offset < entities.size());
|
||||||
|
|
||||||
|
entities[offset] = entities.back();
|
||||||
|
entities[offset]->pool_offset = offset;
|
||||||
|
entities.pop_back();
|
||||||
|
entity_pool.free(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityPool::~EntityPool()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto &list = component_types.inner_list();
|
||||||
|
auto itr = list.begin();
|
||||||
|
while (itr != list.end())
|
||||||
|
{
|
||||||
|
auto *to_free = itr.get();
|
||||||
|
itr = list.erase(itr);
|
||||||
|
delete to_free;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_groups();
|
||||||
|
free_groups();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityDeleter::operator()(Entity *entity)
|
||||||
|
{
|
||||||
|
entity->get_pool()->delete_entity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityPool::free_groups()
|
||||||
|
{
|
||||||
|
auto &list = groups.inner_list();
|
||||||
|
auto itr = list.begin();
|
||||||
|
while (itr != list.end())
|
||||||
|
{
|
||||||
|
auto *to_free = itr.get();
|
||||||
|
itr = list.erase(itr);
|
||||||
|
delete to_free;
|
||||||
|
}
|
||||||
|
groups.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityPool::reset_groups()
|
||||||
|
{
|
||||||
|
for (auto &group : groups)
|
||||||
|
group.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityPool::reset_groups_for_component_type(ComponentType id)
|
||||||
|
{
|
||||||
|
auto *component_groups = component_to_groups.find(id);
|
||||||
|
if (component_groups)
|
||||||
|
{
|
||||||
|
for (auto &group : *component_groups)
|
||||||
|
{
|
||||||
|
auto *g = groups.find(group.get_hash());
|
||||||
|
if (g)
|
||||||
|
g->reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComponentSet::insert(ComponentType type)
|
||||||
|
{
|
||||||
|
set.emplace_yield(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
/* 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 <tuple>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <algorithm>
|
||||||
|
#include "object_pool.hpp"
|
||||||
|
#include "intrusive.hpp"
|
||||||
|
#include "intrusive_hash_map.hpp"
|
||||||
|
#include "compile_time_hash.hpp"
|
||||||
|
#include "enum_cast.hpp"
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
struct ComponentBase
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename Tup>
|
||||||
|
inline T *get_component(Tup &t)
|
||||||
|
{
|
||||||
|
return std::get<T *>(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline T *get(const std::tuple<T *> &t)
|
||||||
|
{
|
||||||
|
return std::get<0>(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
using ComponentGroupVector = std::vector<std::tuple<Ts *...>>;
|
||||||
|
|
||||||
|
class Entity;
|
||||||
|
|
||||||
|
#define GRANITE_COMPONENT_TYPE_HASH(x) ::Util::compile_time_fnv1(#x)
|
||||||
|
using ComponentType = uint64_t;
|
||||||
|
|
||||||
|
#define GRANITE_COMPONENT_TYPE_DECL(x) \
|
||||||
|
enum class ComponentTypeWrapper : ::Granite::ComponentType { \
|
||||||
|
type_id = GRANITE_COMPONENT_TYPE_HASH(x) \
|
||||||
|
}; \
|
||||||
|
static inline constexpr ::Granite::ComponentType get_component_id_hash() { \
|
||||||
|
return ::Granite::ComponentType(ComponentTypeWrapper::type_id); \
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ComponentSetKey : Util::IntrusiveHashMapEnabled<ComponentSetKey>
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
class ComponentSet : public Util::IntrusiveHashMapEnabled<ComponentSet>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void insert(ComponentType type);
|
||||||
|
|
||||||
|
Util::IntrusiveList<ComponentSetKey>::Iterator begin()
|
||||||
|
{
|
||||||
|
return set.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
Util::IntrusiveList<ComponentSetKey>::Iterator end()
|
||||||
|
{
|
||||||
|
return set.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Util::IntrusiveHashMap<ComponentSetKey> set;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ComponentNode = Util::IntrusivePODWrapper<ComponentBase *>;
|
||||||
|
using ComponentHashMap = Util::IntrusiveHashMapHolder<ComponentNode>;
|
||||||
|
using ComponentGroupHashMap = Util::IntrusiveHashMap<ComponentSet>;
|
||||||
|
|
||||||
|
struct ComponentIDMapping
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
constexpr static Util::Hash get_id()
|
||||||
|
{
|
||||||
|
enum class Result : Util::Hash { result = T::get_component_id_hash() };
|
||||||
|
return Util::Hash(Result::result);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
constexpr static Util::Hash get_group_id()
|
||||||
|
{
|
||||||
|
enum class Result : Util::Hash { result = Util::compile_time_fnv1_merged(Ts::get_component_id_hash()...) };
|
||||||
|
return Util::Hash(Result::result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class EntityGroupBase : public Util::IntrusiveHashMapEnabled<EntityGroupBase>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~EntityGroupBase() = default;
|
||||||
|
virtual void add_entity(Entity &entity) = 0;
|
||||||
|
virtual void remove_entity(const Entity &entity) = 0;
|
||||||
|
virtual void reset() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EntityPool;
|
||||||
|
|
||||||
|
struct EntityDeleter
|
||||||
|
{
|
||||||
|
void operator()(Entity *entity);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Entity : public Util::IntrusiveListEnabled<Entity>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
friend class EntityPool;
|
||||||
|
|
||||||
|
Entity(EntityPool *pool_, Util::Hash hash_)
|
||||||
|
: pool(pool_), hash(hash_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_component(ComponentType id) const
|
||||||
|
{
|
||||||
|
auto itr = components.find(id);
|
||||||
|
return itr != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
bool has_component() const
|
||||||
|
{
|
||||||
|
return has_component(ComponentIDMapping::get_id<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T *get_component()
|
||||||
|
{
|
||||||
|
auto *t = components.find(ComponentIDMapping::get_id<T>());
|
||||||
|
if (t)
|
||||||
|
return static_cast<T *>(t->get());
|
||||||
|
else
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
const T *get_component() const
|
||||||
|
{
|
||||||
|
auto *t = components.find(ComponentIDMapping::get_id<T>());
|
||||||
|
if (t)
|
||||||
|
return static_cast<const T *>(t->get());
|
||||||
|
else
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T, typename... Ts>
|
||||||
|
T *allocate_component(Ts&&... ts);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void free_component();
|
||||||
|
|
||||||
|
ComponentHashMap &get_components()
|
||||||
|
{
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityPool *get_pool()
|
||||||
|
{
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
Util::Hash get_hash() const
|
||||||
|
{
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mark_for_destruction()
|
||||||
|
{
|
||||||
|
bool ret = !marked;
|
||||||
|
marked = true;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
EntityPool *pool;
|
||||||
|
Util::Hash hash;
|
||||||
|
size_t pool_offset = 0;
|
||||||
|
ComponentHashMap components;
|
||||||
|
bool marked = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
class EntityGroup : public EntityGroupBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void add_entity(Entity &entity) override final
|
||||||
|
{
|
||||||
|
if (has_all_components<Ts...>(entity))
|
||||||
|
{
|
||||||
|
entity_to_index[entity.get_hash()].get() = entities.size();
|
||||||
|
groups.push_back(std::make_tuple(entity.get_component<Ts>()...));
|
||||||
|
entities.push_back(&entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove_entity(const Entity &entity) override final
|
||||||
|
{
|
||||||
|
size_t offset;
|
||||||
|
if (entity_to_index.find_and_consume_pod(entity.get_hash(), offset))
|
||||||
|
{
|
||||||
|
entities[offset] = entities.back();
|
||||||
|
groups[offset] = groups.back();
|
||||||
|
entity_to_index[entities[offset]->get_hash()].get() = offset;
|
||||||
|
|
||||||
|
entity_to_index.erase(entity.get_hash());
|
||||||
|
entities.pop_back();
|
||||||
|
groups.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComponentGroupVector<Ts...> &get_groups() const
|
||||||
|
{
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Entity *> &get_entities() const
|
||||||
|
{
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() override final
|
||||||
|
{
|
||||||
|
groups.clear();
|
||||||
|
entities.clear();
|
||||||
|
entity_to_index.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
ComponentGroupVector<Ts...> groups;
|
||||||
|
std::vector<Entity *> entities;
|
||||||
|
Util::IntrusiveHashMap<Util::IntrusivePODWrapper<size_t>> entity_to_index;
|
||||||
|
|
||||||
|
template <typename... Us>
|
||||||
|
struct HasAllComponents;
|
||||||
|
|
||||||
|
template <typename U, typename... Us>
|
||||||
|
struct HasAllComponents<U, Us...>
|
||||||
|
{
|
||||||
|
static bool has_component(const Entity &entity)
|
||||||
|
{
|
||||||
|
return entity.has_component(ComponentIDMapping::get_id<U>()) &&
|
||||||
|
HasAllComponents<Us...>::has_component(entity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename U>
|
||||||
|
struct HasAllComponents<U>
|
||||||
|
{
|
||||||
|
static bool has_component(const Entity &entity)
|
||||||
|
{
|
||||||
|
return entity.has_component(ComponentIDMapping::get_id<U>());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename... Us>
|
||||||
|
bool has_all_components(const Entity &entity)
|
||||||
|
{
|
||||||
|
return HasAllComponents<Us...>::has_component(entity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ComponentAllocatorBase : public Util::IntrusiveHashMapEnabled<ComponentAllocatorBase>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~ComponentAllocatorBase() = default;
|
||||||
|
virtual void free_component(ComponentBase *component) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct ComponentAllocator : public ComponentAllocatorBase
|
||||||
|
{
|
||||||
|
Util::ObjectPool<T> pool;
|
||||||
|
|
||||||
|
void free_component(ComponentBase *component) override final
|
||||||
|
{
|
||||||
|
pool.free(static_cast<T *>(component));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class EntityPool
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
~EntityPool();
|
||||||
|
|
||||||
|
EntityPool() = default;
|
||||||
|
void operator=(const EntityPool &) = delete;
|
||||||
|
EntityPool(const EntityPool &) = delete;
|
||||||
|
|
||||||
|
Entity *create_entity();
|
||||||
|
void delete_entity(Entity *entity);
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
EntityGroup<Ts...> *get_component_group_holder()
|
||||||
|
{
|
||||||
|
ComponentType group_id = ComponentIDMapping::get_group_id<Ts...>();
|
||||||
|
auto *t = groups.find(group_id);
|
||||||
|
if (!t)
|
||||||
|
{
|
||||||
|
register_group<Ts...>(group_id);
|
||||||
|
|
||||||
|
t = new EntityGroup<Ts...>();
|
||||||
|
t->set_hash(group_id);
|
||||||
|
groups.insert_yield(t);
|
||||||
|
|
||||||
|
auto *group = static_cast<EntityGroup<Ts...> *>(t);
|
||||||
|
for (auto &entity : entities)
|
||||||
|
group->add_entity(*entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<EntityGroup<Ts...> *>(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
const ComponentGroupVector<Ts...> &get_component_group()
|
||||||
|
{
|
||||||
|
auto *group = get_component_group_holder<Ts...>();
|
||||||
|
return group->get_groups();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
const std::vector<Entity *> &get_component_entities()
|
||||||
|
{
|
||||||
|
auto *group = get_component_group_holder<Ts...>();
|
||||||
|
return group->get_entities();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T, typename... Ts>
|
||||||
|
T *allocate_component(Entity &entity, Ts&&... ts)
|
||||||
|
{
|
||||||
|
constexpr ComponentType id = ComponentIDMapping::get_id<T>();
|
||||||
|
auto *t = component_types.find(id);
|
||||||
|
if (!t)
|
||||||
|
{
|
||||||
|
t = new ComponentAllocator<T>();
|
||||||
|
t->set_hash(id);
|
||||||
|
component_types.insert_yield(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *allocator = static_cast<ComponentAllocator<T> *>(t);
|
||||||
|
auto *existing = entity.components.find(id);
|
||||||
|
|
||||||
|
if (existing)
|
||||||
|
{
|
||||||
|
auto *comp = static_cast<T *>(existing->get());
|
||||||
|
// In-place modify. Destroy old data, and in-place construct.
|
||||||
|
// Do not need to fiddle with data structures internally.
|
||||||
|
comp->~T();
|
||||||
|
return new (comp) T(std::forward<Ts>(ts)...);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto *comp = allocator->pool.allocate(std::forward<Ts>(ts)...);
|
||||||
|
auto *node = component_nodes.allocate(comp);
|
||||||
|
node->set_hash(id);
|
||||||
|
entity.components.insert_replace(node);
|
||||||
|
|
||||||
|
auto *component_groups = component_to_groups.find(id);
|
||||||
|
if (component_groups)
|
||||||
|
for (auto &group : *component_groups)
|
||||||
|
groups.find(group.get_hash())->add_entity(entity);
|
||||||
|
|
||||||
|
return comp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void free_component(Entity &entity, ComponentType id, ComponentNode *component);
|
||||||
|
void reset_groups();
|
||||||
|
void reset_groups_for_component_type(ComponentType id);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Util::ObjectPool<Entity> entity_pool;
|
||||||
|
Util::IntrusiveHashMapHolder<EntityGroupBase> groups;
|
||||||
|
Util::IntrusiveHashMapHolder<ComponentAllocatorBase> component_types;
|
||||||
|
Util::ObjectPool<ComponentNode> component_nodes;
|
||||||
|
ComponentGroupHashMap component_to_groups;
|
||||||
|
std::vector<Entity *> entities;
|
||||||
|
uint64_t cookie = 0;
|
||||||
|
|
||||||
|
template <typename... Us>
|
||||||
|
struct GroupRegisters;
|
||||||
|
|
||||||
|
template <typename U, typename... Us>
|
||||||
|
struct GroupRegisters<U, Us...>
|
||||||
|
{
|
||||||
|
static void register_group(ComponentGroupHashMap &groups,
|
||||||
|
ComponentType group_id)
|
||||||
|
{
|
||||||
|
groups.emplace_yield(ComponentIDMapping::get_id<U>())->insert(group_id);
|
||||||
|
GroupRegisters<Us...>::register_group(groups, group_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename U>
|
||||||
|
struct GroupRegisters<U>
|
||||||
|
{
|
||||||
|
static void register_group(ComponentGroupHashMap &groups,
|
||||||
|
ComponentType group_id)
|
||||||
|
{
|
||||||
|
groups.emplace_yield(ComponentIDMapping::get_id<U>())->insert(group_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename U, typename... Us>
|
||||||
|
void register_group(ComponentType group_id)
|
||||||
|
{
|
||||||
|
GroupRegisters<U, Us...>::register_group(component_to_groups, group_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void free_groups();
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename... Ts>
|
||||||
|
T *Entity::allocate_component(Ts&&... ts)
|
||||||
|
{
|
||||||
|
return pool->allocate_component<T>(*this, std::forward<Ts>(ts)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void Entity::free_component()
|
||||||
|
{
|
||||||
|
auto id = ComponentIDMapping::get_id<T>();
|
||||||
|
auto *t = components.find(id);
|
||||||
|
if (t)
|
||||||
|
{
|
||||||
|
components.erase(t);
|
||||||
|
pool->free_component(*this, t->get_hash(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
add_granite_internal_lib(granite-event event.hpp event.cpp)
|
||||||
|
target_include_directories(granite-event PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(granite-event PUBLIC granite-util granite-application-global)
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/* 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 "event.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
EventManager::~EventManager()
|
||||||
|
{
|
||||||
|
dispatch();
|
||||||
|
for (auto &event_type : latched_events)
|
||||||
|
{
|
||||||
|
for (auto &handler : event_type.handlers)
|
||||||
|
{
|
||||||
|
dispatch_down_events(event_type.queued_events, handler);
|
||||||
|
// Before the event manager dies, make sure no stale EventHandler objects try to unregister themselves.
|
||||||
|
handler.unregister_key->release_manager_reference();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::dispatch()
|
||||||
|
{
|
||||||
|
for (auto &event_type : events)
|
||||||
|
{
|
||||||
|
auto &handlers = event_type.handlers;
|
||||||
|
auto &queued_events = event_type.queued_events;
|
||||||
|
auto itr = remove_if(begin(handlers), end(handlers), [&](const Handler &handler) {
|
||||||
|
for (auto &event : queued_events)
|
||||||
|
{
|
||||||
|
if (!handler.mem_fn(handler.handler, *event))
|
||||||
|
{
|
||||||
|
handler.unregister_key->release_manager_reference();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
handlers.erase(itr, end(handlers));
|
||||||
|
queued_events.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::dispatch_event(std::vector<Handler> &handlers, const Event &e)
|
||||||
|
{
|
||||||
|
auto itr = remove_if(begin(handlers), end(handlers), [&](const Handler &handler) -> bool {
|
||||||
|
bool to_remove = !handler.mem_fn(handler.handler, e);
|
||||||
|
if (to_remove)
|
||||||
|
handler.unregister_key->release_manager_reference();
|
||||||
|
return to_remove;
|
||||||
|
});
|
||||||
|
|
||||||
|
handlers.erase(itr, end(handlers));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::dispatch_up_events(std::vector<std::unique_ptr<Event>> &up_events, const LatchHandler &handler)
|
||||||
|
{
|
||||||
|
for (auto &event : up_events)
|
||||||
|
handler.up_fn(handler.handler, *event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::dispatch_down_events(std::vector<std::unique_ptr<Event>> &down_events, const LatchHandler &handler)
|
||||||
|
{
|
||||||
|
for (auto &event : down_events)
|
||||||
|
handler.down_fn(handler.handler, *event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::LatchEventTypeData::flush_recursive_handlers()
|
||||||
|
{
|
||||||
|
handlers.insert(end(handlers), begin(recursive_handlers), end(recursive_handlers));
|
||||||
|
recursive_handlers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::EventTypeData::flush_recursive_handlers()
|
||||||
|
{
|
||||||
|
handlers.insert(end(handlers), begin(recursive_handlers), end(recursive_handlers));
|
||||||
|
recursive_handlers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::dispatch_up_event(LatchEventTypeData &event_type, const Event &event)
|
||||||
|
{
|
||||||
|
event_type.dispatching = true;
|
||||||
|
for (auto &handler : event_type.handlers)
|
||||||
|
handler.up_fn(handler.handler, event);
|
||||||
|
event_type.flush_recursive_handlers();
|
||||||
|
event_type.dispatching = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::dispatch_down_event(LatchEventTypeData &event_type, const Event &event)
|
||||||
|
{
|
||||||
|
event_type.dispatching = true;
|
||||||
|
for (auto &handler : event_type.handlers)
|
||||||
|
handler.down_fn(handler.handler, event);
|
||||||
|
event_type.flush_recursive_handlers();
|
||||||
|
event_type.dispatching = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::unregister_handler(EventHandler *handler)
|
||||||
|
{
|
||||||
|
for (auto &event_type : events)
|
||||||
|
{
|
||||||
|
auto itr = remove_if(begin(event_type.handlers), end(event_type.handlers), [&](const Handler &h) -> bool {
|
||||||
|
bool to_remove = h.unregister_key == handler;
|
||||||
|
if (to_remove)
|
||||||
|
h.unregister_key->release_manager_reference();
|
||||||
|
return to_remove;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itr != end(event_type.handlers) && event_type.dispatching)
|
||||||
|
throw std::logic_error("Unregistering handlers while dispatching events.");
|
||||||
|
|
||||||
|
if (itr != end(event_type.handlers))
|
||||||
|
event_type.handlers.erase(itr, end(event_type.handlers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::unregister_latch_handler(EventHandler *handler)
|
||||||
|
{
|
||||||
|
for (auto &event_type : latched_events)
|
||||||
|
{
|
||||||
|
auto itr = remove_if(begin(event_type.handlers), end(event_type.handlers), [&](const LatchHandler &h) -> bool {
|
||||||
|
bool to_remove = h.unregister_key == handler;
|
||||||
|
if (to_remove)
|
||||||
|
h.unregister_key->release_manager_reference();
|
||||||
|
return to_remove;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itr != end(event_type.handlers))
|
||||||
|
event_type.handlers.erase(itr, end(event_type.handlers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::dequeue_latched(uint64_t cookie)
|
||||||
|
{
|
||||||
|
for (auto &event_type : latched_events)
|
||||||
|
{
|
||||||
|
auto &queued_events = event_type.queued_events;
|
||||||
|
if (event_type.enqueueing)
|
||||||
|
throw std::logic_error("Dequeueing latched while queueing events.");
|
||||||
|
event_type.enqueueing = true;
|
||||||
|
|
||||||
|
auto itr = remove_if(begin(queued_events), end(queued_events), [&](const std::unique_ptr<Event> &event) {
|
||||||
|
bool signal = event->get_cookie() == cookie;
|
||||||
|
if (signal)
|
||||||
|
dispatch_down_event(event_type, *event);
|
||||||
|
return signal;
|
||||||
|
});
|
||||||
|
|
||||||
|
event_type.enqueueing = false;
|
||||||
|
queued_events.erase(itr, end(queued_events));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventManager::dequeue_all_latched(EventType type)
|
||||||
|
{
|
||||||
|
auto &event_type = latched_events[type];
|
||||||
|
if (event_type.enqueueing)
|
||||||
|
throw std::logic_error("Dequeueing latched while queueing events.");
|
||||||
|
|
||||||
|
event_type.enqueueing = true;
|
||||||
|
for (auto &event : event_type.queued_events)
|
||||||
|
dispatch_down_event(event_type, *event);
|
||||||
|
event_type.queued_events.clear();
|
||||||
|
event_type.enqueueing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::release_manager_reference()
|
||||||
|
{
|
||||||
|
assert(event_manager_ref_count > 0);
|
||||||
|
assert(event_manager);
|
||||||
|
if (--event_manager_ref_count == 0)
|
||||||
|
event_manager = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::add_manager_reference(EventManager *manager)
|
||||||
|
{
|
||||||
|
assert(!event_manager_ref_count || manager == event_manager);
|
||||||
|
event_manager = manager;
|
||||||
|
event_manager_ref_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventHandler::~EventHandler()
|
||||||
|
{
|
||||||
|
if (event_manager)
|
||||||
|
event_manager->unregister_handler(this);
|
||||||
|
// Splitting the branch is significant since event manager can release its last reference in between.
|
||||||
|
if (event_manager)
|
||||||
|
event_manager->unregister_latch_handler(this);
|
||||||
|
assert(event_manager_ref_count == 0 && !event_manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
/* 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 <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
#include "compile_time_hash.hpp"
|
||||||
|
#include "intrusive_hash_map.hpp"
|
||||||
|
#include "global_managers.hpp"
|
||||||
|
|
||||||
|
#define EVENT_MANAGER_REGISTER(clazz, member, event) \
|
||||||
|
GRANITE_EVENT_MANAGER()->register_handler<clazz, event, &clazz::member>(this)
|
||||||
|
#define EVENT_MANAGER_REGISTER_LATCH(clazz, up_event, down_event, event) \
|
||||||
|
GRANITE_EVENT_MANAGER()->register_latch_handler<clazz, event, &clazz::up_event, &clazz::down_event>(this)
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class Event;
|
||||||
|
|
||||||
|
template <typename Return, typename T, typename EventType, Return (T::*callback)(const EventType &e)>
|
||||||
|
Return member_function_invoker(void *object, const Event &e)
|
||||||
|
{
|
||||||
|
return (static_cast<T *>(object)->*callback)(static_cast<const EventType &>(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
#define GRANITE_EVENT_TYPE_HASH(x) ::Util::compile_time_fnv1(#x)
|
||||||
|
using EventType = uint64_t;
|
||||||
|
|
||||||
|
#define GRANITE_EVENT_TYPE_DECL(x) \
|
||||||
|
enum class EventTypeWrapper : ::Granite::EventType { \
|
||||||
|
type_id = GRANITE_EVENT_TYPE_HASH(x) \
|
||||||
|
}; \
|
||||||
|
static inline constexpr ::Granite::EventType get_type_id() { \
|
||||||
|
return ::Granite::EventType(EventTypeWrapper::type_id); \
|
||||||
|
}
|
||||||
|
|
||||||
|
class Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~Event() = default;
|
||||||
|
|
||||||
|
Event() = default;
|
||||||
|
|
||||||
|
// Doesn't have to be set unless type information is going to be lost.
|
||||||
|
// E.g. we're storing the Event some place and we have type-erasure.
|
||||||
|
// Having this set helps us recover type information for dispatch.
|
||||||
|
explicit Event(EventType type_)
|
||||||
|
: type(type_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType get_type_id() const
|
||||||
|
{
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_cookie(uint64_t cookie_)
|
||||||
|
{
|
||||||
|
cookie = cookie_;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t get_cookie() const
|
||||||
|
{
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
EventType type = 0;
|
||||||
|
uint64_t cookie = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EventManager;
|
||||||
|
|
||||||
|
class EventHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
EventHandler(const EventHandler &) = delete;
|
||||||
|
void operator=(const EventHandler &) = delete;
|
||||||
|
EventHandler() = default;
|
||||||
|
~EventHandler();
|
||||||
|
|
||||||
|
void add_manager_reference(EventManager *manager);
|
||||||
|
void release_manager_reference();
|
||||||
|
|
||||||
|
private:
|
||||||
|
EventManager *event_manager = nullptr;
|
||||||
|
uint32_t event_manager_ref_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EventManager final : public EventManagerInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
template<typename T, typename... P>
|
||||||
|
void enqueue(P&&... p)
|
||||||
|
{
|
||||||
|
static constexpr auto type = T::get_type_id();
|
||||||
|
auto &l = events[type];
|
||||||
|
|
||||||
|
auto ptr = std::unique_ptr<Event>(new T(std::forward<P>(p)...));
|
||||||
|
l.queued_events.emplace_back(std::move(ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T, typename... P>
|
||||||
|
uint64_t enqueue_latched(P&&... p)
|
||||||
|
{
|
||||||
|
static constexpr auto type = T::get_type_id();
|
||||||
|
auto &l = latched_events[type];
|
||||||
|
auto ptr = std::unique_ptr<Event>(new T(std::forward<P>(p)...));
|
||||||
|
uint64_t cookie = ++cookie_counter;
|
||||||
|
ptr->set_cookie(cookie);
|
||||||
|
|
||||||
|
if (l.enqueueing)
|
||||||
|
throw std::logic_error("Cannot enqueue more latched events while handling events.");
|
||||||
|
l.enqueueing = true;
|
||||||
|
|
||||||
|
auto *event = ptr.get();
|
||||||
|
l.queued_events.emplace_back(std::move(ptr));
|
||||||
|
dispatch_up_event(l, *event);
|
||||||
|
l.enqueueing = false;
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dequeue_latched(uint64_t cookie);
|
||||||
|
void dequeue_all_latched(EventType type);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void dispatch_inline(const T &t)
|
||||||
|
{
|
||||||
|
static constexpr auto type = T::get_type_id();
|
||||||
|
auto &l = events[type];
|
||||||
|
dispatch_event(l.handlers, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatch_inline(const Event &e)
|
||||||
|
{
|
||||||
|
assert(e.get_type_id() != 0);
|
||||||
|
auto &l = events[e.get_type_id()];
|
||||||
|
dispatch_event(l.handlers, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatch();
|
||||||
|
|
||||||
|
template<typename T, typename EventType, bool (T::*mem_fn)(const EventType &)>
|
||||||
|
void register_handler(T *handler)
|
||||||
|
{
|
||||||
|
handler->add_manager_reference(this);
|
||||||
|
static constexpr auto type_id = EventType::get_type_id();
|
||||||
|
auto &l = events[type_id];
|
||||||
|
if (l.dispatching)
|
||||||
|
l.recursive_handlers.push_back({ member_function_invoker<bool, T, EventType, mem_fn>, handler, handler });
|
||||||
|
else
|
||||||
|
l.handlers.push_back({ member_function_invoker<bool, T, EventType, mem_fn>, handler, handler });
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregister_handler(EventHandler *handler);
|
||||||
|
|
||||||
|
template<typename T, typename EventType, void (T::*up_fn)(const EventType &), void (T::*down_fn)(const EventType &)>
|
||||||
|
void register_latch_handler(T *handler)
|
||||||
|
{
|
||||||
|
handler->add_manager_reference(this);
|
||||||
|
LatchHandler h{
|
||||||
|
member_function_invoker<void, T, EventType, up_fn>,
|
||||||
|
member_function_invoker<void, T, EventType, down_fn>,
|
||||||
|
handler, handler };
|
||||||
|
|
||||||
|
static constexpr auto type_id = EventType::get_type_id();
|
||||||
|
auto &levents = latched_events[type_id];
|
||||||
|
dispatch_up_events(levents.queued_events, h);
|
||||||
|
|
||||||
|
auto &l = latched_events[type_id];
|
||||||
|
if (l.dispatching)
|
||||||
|
l.recursive_handlers.push_back(h);
|
||||||
|
else
|
||||||
|
l.handlers.push_back(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregister_latch_handler(EventHandler *handler);
|
||||||
|
|
||||||
|
~EventManager();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Handler
|
||||||
|
{
|
||||||
|
bool (*mem_fn)(void *object, const Event &event);
|
||||||
|
void *handler;
|
||||||
|
EventHandler *unregister_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LatchHandler
|
||||||
|
{
|
||||||
|
void (*up_fn)(void *object, const Event &event);
|
||||||
|
void (*down_fn)(void *object, const Event &event);
|
||||||
|
void *handler;
|
||||||
|
EventHandler *unregister_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EventTypeData : Util::IntrusiveHashMapEnabled<EventTypeData>
|
||||||
|
{
|
||||||
|
std::vector<std::unique_ptr<Event>> queued_events;
|
||||||
|
std::vector<Handler> handlers;
|
||||||
|
std::vector<Handler> recursive_handlers;
|
||||||
|
bool enqueueing = false;
|
||||||
|
bool dispatching = false;
|
||||||
|
|
||||||
|
void flush_recursive_handlers();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LatchEventTypeData : Util::IntrusiveHashMapEnabled<LatchEventTypeData>
|
||||||
|
{
|
||||||
|
std::vector<std::unique_ptr<Event>> queued_events;
|
||||||
|
std::vector<LatchHandler> handlers;
|
||||||
|
std::vector<LatchHandler> recursive_handlers;
|
||||||
|
bool enqueueing = false;
|
||||||
|
bool dispatching = false;
|
||||||
|
|
||||||
|
void flush_recursive_handlers();
|
||||||
|
};
|
||||||
|
|
||||||
|
void dispatch_event(std::vector<Handler> &handlers, const Event &e);
|
||||||
|
void dispatch_up_events(std::vector<std::unique_ptr<Event>> &events, const LatchHandler &handler);
|
||||||
|
void dispatch_down_events(std::vector<std::unique_ptr<Event>> &events, const LatchHandler &handler);
|
||||||
|
void dispatch_up_event(LatchEventTypeData &event_type, const Event &event);
|
||||||
|
void dispatch_down_event(LatchEventTypeData &event_type, const Event &event);
|
||||||
|
|
||||||
|
Util::IntrusiveHashMap<EventTypeData> events;
|
||||||
|
Util::IntrusiveHashMap<LatchEventTypeData> latched_events;
|
||||||
|
uint64_t cookie_counter = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
add_granite_internal_lib(granite-filesystem
|
||||||
|
volatile_source.hpp
|
||||||
|
filesystem.hpp filesystem.cpp
|
||||||
|
asset_manager.cpp asset_manager.hpp)
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
target_sources(granite-filesystem PRIVATE windows/os_filesystem.cpp windows/os_filesystem.hpp)
|
||||||
|
target_include_directories(granite-filesystem PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/windows)
|
||||||
|
elseif (ANDROID)
|
||||||
|
target_sources(granite-filesystem PRIVATE linux/os_filesystem.cpp linux/os_filesystem.hpp)
|
||||||
|
target_sources(granite-filesystem PRIVATE android/android.cpp android/android.hpp)
|
||||||
|
target_include_directories(granite-filesystem PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/linux)
|
||||||
|
target_include_directories(granite-filesystem PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/android)
|
||||||
|
else()
|
||||||
|
target_sources(granite-filesystem PRIVATE linux/os_filesystem.cpp linux/os_filesystem.hpp)
|
||||||
|
target_include_directories(granite-filesystem PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/linux)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_include_directories(granite-filesystem PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(granite-filesystem PUBLIC granite-util granite-path granite-application-global PRIVATE granite-threading)
|
||||||
|
|
||||||
|
if (GRANITE_SHIPPING)
|
||||||
|
target_compile_definitions(granite-filesystem PRIVATE GRANITE_SHIPPING)
|
||||||
|
else()
|
||||||
|
target_compile_definitions(granite-filesystem PRIVATE GRANITE_DEFAULT_BUILTIN_DIRECTORY=\"${CMAKE_CURRENT_SOURCE_DIR}/../assets\")
|
||||||
|
target_compile_definitions(granite-filesystem PRIVATE GRANITE_DEFAULT_CACHE_DIRECTORY=\"${CMAKE_BINARY_DIR}/cache\")
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/* 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 "android.hpp"
|
||||||
|
#include "path_utils.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
bool AssetFile::init(AAssetManager *mgr, const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
if (mode != FileMode::ReadOnly)
|
||||||
|
{
|
||||||
|
LOGE("Asset files must be opened read-only.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
asset = AAssetManager_open(mgr, path.c_str(), AASSET_MODE_BUFFER);
|
||||||
|
if (!asset)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
size = AAsset_getLength64(asset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHandle AssetFile::open(AAssetManager *mgr, const std::string &path, Granite::FileMode mode)
|
||||||
|
{
|
||||||
|
auto file = Util::make_handle<AssetFile>();
|
||||||
|
if (!file->init(mgr, path, mode))
|
||||||
|
file.reset();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle AssetFile::map_subset(uint64_t offset, size_t range)
|
||||||
|
{
|
||||||
|
if (offset + range > size)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
auto *data = static_cast<uint8_t *>(const_cast<void *>(AAsset_getBuffer(asset)));
|
||||||
|
if (!data)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return Util::make_handle<FileMapping>(
|
||||||
|
reference_from_this(), offset,
|
||||||
|
data + offset, range,
|
||||||
|
0, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t AssetFile::get_size()
|
||||||
|
{
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle AssetFile::map_write(size_t)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetFile::unmap(void *, size_t)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetFile::~AssetFile()
|
||||||
|
{
|
||||||
|
if (asset)
|
||||||
|
AAsset_close(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetManagerFilesystem::AssetManagerFilesystem(const std::string &base_)
|
||||||
|
: base(base_), mgr(global_asset_manager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHandle AssetManagerFilesystem::open(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
return AssetFile::open(mgr, Path::join(base, Path::canonicalize_path(path)), mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
int AssetManagerFilesystem::get_notification_fd() const
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManagerFilesystem::poll_notifications()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManagerFilesystem::uninstall_notification(FileNotifyHandle)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FileNotifyHandle AssetManagerFilesystem::install_notification(const std::string &,
|
||||||
|
std::function<void (const FileNotifyInfo &)>)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListEntry> AssetManagerFilesystem::list(const std::string &path)
|
||||||
|
{
|
||||||
|
auto directory = Path::join(base, Path::canonicalize_path(path));
|
||||||
|
auto *dir = AAssetManager_openDir(mgr, directory.c_str());
|
||||||
|
|
||||||
|
if (!dir)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
std::vector<ListEntry> entries;
|
||||||
|
|
||||||
|
const char *entry;
|
||||||
|
while ((entry = AAssetDir_getNextFileName(dir)))
|
||||||
|
{
|
||||||
|
PathType type = PathType::File;
|
||||||
|
entries.push_back({ entry, type });
|
||||||
|
}
|
||||||
|
|
||||||
|
AAssetDir_close(dir);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AssetManagerFilesystem::stat(const std::string &path, FileStat &stat)
|
||||||
|
{
|
||||||
|
auto resolved_path = Path::join(base, Path::canonicalize_path(path));
|
||||||
|
|
||||||
|
auto *asset = AAssetManager_open(mgr, resolved_path.c_str(), AASSET_MODE_UNKNOWN);
|
||||||
|
if (!asset)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
stat.size = AAsset_getLength(asset);
|
||||||
|
stat.type = PathType::File;
|
||||||
|
stat.last_modified = 0;
|
||||||
|
AAsset_close(asset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AAssetManager *AssetManagerFilesystem::global_asset_manager;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/* 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 "../filesystem.hpp"
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <android/asset_manager_jni.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class AssetFile final : public File
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static FileHandle open(AAssetManager *mgr, const std::string &path, FileMode mode);
|
||||||
|
~AssetFile() override;
|
||||||
|
FileMappingHandle map_subset(uint64_t offset, size_t range) override;
|
||||||
|
FileMappingHandle map_write(size_t size) override;
|
||||||
|
void unmap(void *mapped, size_t mapped_range) override;
|
||||||
|
uint64_t get_size() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool init(AAssetManager *mgr, const std::string &path, FileMode mode);
|
||||||
|
AAsset *asset = nullptr;
|
||||||
|
size_t size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AssetManagerFilesystem : public FilesystemBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AssetManagerFilesystem(const std::string &base);
|
||||||
|
std::vector<ListEntry> list(const std::string &path) override;
|
||||||
|
FileHandle open(const std::string &path, FileMode mode) override;
|
||||||
|
bool stat(const std::string &path, FileStat &stat) override;
|
||||||
|
FileNotifyHandle install_notification(const std::string &path, std::function<void (const FileNotifyInfo &)> func) override;
|
||||||
|
void uninstall_notification(FileNotifyHandle handle) override;
|
||||||
|
void poll_notifications() override;
|
||||||
|
int get_notification_fd() const override;
|
||||||
|
|
||||||
|
static AAssetManager *global_asset_manager;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string base;
|
||||||
|
AAssetManager *mgr;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
/* 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 "asset_manager.hpp"
|
||||||
|
#include "thread_group.hpp"
|
||||||
|
#include <utility>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
AssetManager::AssetManager()
|
||||||
|
{
|
||||||
|
asset_bank.reserve(AssetID::MaxIDs);
|
||||||
|
sorted_assets.reserve(AssetID::MaxIDs);
|
||||||
|
signal = std::make_unique<TaskSignal>();
|
||||||
|
for (uint64_t i = 0; i < timestamp; i++)
|
||||||
|
signal->signal_increment();
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetManager::~AssetManager()
|
||||||
|
{
|
||||||
|
set_asset_instantiator_interface(nullptr);
|
||||||
|
signal->wait_until_at_least(timestamp);
|
||||||
|
for (uint32_t i = 0; i < id_count; i++)
|
||||||
|
pool.free(asset_bank[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetID AssetManager::register_asset_nolock(FileHandle file, AssetClass asset_class, int prio)
|
||||||
|
{
|
||||||
|
auto *info = pool.allocate();
|
||||||
|
info->handle = std::move(file);
|
||||||
|
info->id.id = id_count;
|
||||||
|
info->prio = prio;
|
||||||
|
info->asset_class = asset_class;
|
||||||
|
AssetID ret = info->id;
|
||||||
|
asset_bank[id_count++] = info;
|
||||||
|
if (iface)
|
||||||
|
{
|
||||||
|
iface->set_id_bounds(id_count);
|
||||||
|
iface->set_asset_class(info->id, asset_class);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetInstantiatorInterface::set_asset_class(AssetID, AssetClass)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetID AssetManager::register_asset(FileHandle file, AssetClass asset_class, int prio)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
||||||
|
return register_asset_nolock(std::move(file), asset_class, prio);
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetID AssetManager::register_asset(Filesystem &fs, const std::string &path, AssetClass asset_class, int prio)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
||||||
|
|
||||||
|
Util::Hasher h;
|
||||||
|
h.string(path);
|
||||||
|
if (auto *asset = file_to_assets.find(h.get()))
|
||||||
|
return asset->id;
|
||||||
|
|
||||||
|
auto file = fs.open(path);
|
||||||
|
if (!file)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
auto id = register_asset_nolock(std::move(file), asset_class, prio);
|
||||||
|
asset_bank[id.id]->set_hash(h.get());
|
||||||
|
file_to_assets.insert_replace(asset_bank[id.id]);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::update_cost(AssetID id, uint64_t cost)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{cost_update_lock};
|
||||||
|
thread_cost_updates.push_back({ id, cost });
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::set_asset_instantiator_interface(AssetInstantiatorInterface *iface_)
|
||||||
|
{
|
||||||
|
if (iface)
|
||||||
|
{
|
||||||
|
signal->wait_until_at_least(timestamp);
|
||||||
|
for (uint32_t id = 0; id < id_count; id++)
|
||||||
|
iface->release_asset(AssetID{id});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < id_count; i++)
|
||||||
|
{
|
||||||
|
auto *a = asset_bank[i];
|
||||||
|
a->consumed = 0;
|
||||||
|
a->pending_consumed = 0;
|
||||||
|
a->last_used = 0;
|
||||||
|
}
|
||||||
|
total_consumed = 0;
|
||||||
|
|
||||||
|
iface = iface_;
|
||||||
|
if (iface)
|
||||||
|
{
|
||||||
|
iface->set_id_bounds(id_count);
|
||||||
|
for (uint32_t i = 0; i < id_count; i++)
|
||||||
|
iface->set_asset_class(AssetID{i}, asset_bank[i]->asset_class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::mark_used_asset(AssetID id)
|
||||||
|
{
|
||||||
|
lru_append.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AssetManager::get_wants_mesh_assets() const
|
||||||
|
{
|
||||||
|
return wants_mesh_assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::enable_mesh_assets()
|
||||||
|
{
|
||||||
|
wants_mesh_assets = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::set_asset_budget(uint64_t cost)
|
||||||
|
{
|
||||||
|
transfer_budget = cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::set_asset_budget_per_iteration(uint64_t cost)
|
||||||
|
{
|
||||||
|
transfer_budget_per_iteration = cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AssetManager::set_asset_residency_priority(AssetID id, int prio)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
||||||
|
if (id.id >= id_count)
|
||||||
|
return false;
|
||||||
|
asset_bank[id.id]->prio = prio;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::adjust_update(const CostUpdate &update)
|
||||||
|
{
|
||||||
|
if (update.id.id < id_count)
|
||||||
|
{
|
||||||
|
auto *a = asset_bank[update.id.id];
|
||||||
|
total_consumed += update.cost - (a->consumed + a->pending_consumed);
|
||||||
|
a->consumed = update.cost;
|
||||||
|
a->pending_consumed = 0;
|
||||||
|
|
||||||
|
// A recently paged in image shouldn't be paged out right away in a situation where we're thrashing,
|
||||||
|
// that'd be very dumb.
|
||||||
|
a->last_used = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t AssetManager::get_current_total_consumed() const
|
||||||
|
{
|
||||||
|
return total_consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::update_costs_locked_assets()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> holder_cost{cost_update_lock};
|
||||||
|
std::swap(cost_updates, thread_cost_updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &update : cost_updates)
|
||||||
|
adjust_update(update);
|
||||||
|
cost_updates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::update_lru_locked_assets()
|
||||||
|
{
|
||||||
|
lru_append.for_each_ranged([this](const AssetID *id, size_t count) {
|
||||||
|
for (size_t i = 0; i < count; i++)
|
||||||
|
if (id[i].id < id_count)
|
||||||
|
asset_bank[id[i].id]->last_used = timestamp;
|
||||||
|
});
|
||||||
|
lru_append.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AssetManager::iterate_blocking(ThreadGroup &group, AssetID id)
|
||||||
|
{
|
||||||
|
if (!iface)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
||||||
|
update_costs_locked_assets();
|
||||||
|
update_lru_locked_assets();
|
||||||
|
|
||||||
|
if (id.id >= id_count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto *candidate = asset_bank[id.id];
|
||||||
|
if (candidate->consumed != 0 || candidate->pending_consumed != 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
uint64_t estimate = iface->estimate_cost_asset(candidate->id, *candidate->handle);
|
||||||
|
auto task = group.create_task();
|
||||||
|
task->set_task_class(TaskClass::Background);
|
||||||
|
task->set_fence_counter_signal(signal.get());
|
||||||
|
task->set_desc("asset-manager-instantiate-single");
|
||||||
|
iface->instantiate_asset(*this, task.get(), candidate->id, *candidate->handle);
|
||||||
|
candidate->pending_consumed = estimate;
|
||||||
|
candidate->last_used = timestamp;
|
||||||
|
total_consumed += estimate;
|
||||||
|
|
||||||
|
// We cannot increment the timestamp here, remember this for later.
|
||||||
|
// We hold a lock on the asset bank here, so this is fine even if called concurrently.
|
||||||
|
blocking_signals++;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::iterate(ThreadGroup *group)
|
||||||
|
{
|
||||||
|
if (!iface)
|
||||||
|
return;
|
||||||
|
|
||||||
|
timestamp += blocking_signals;
|
||||||
|
blocking_signals = 0;
|
||||||
|
|
||||||
|
// If there is too much pending work in flight, skip.
|
||||||
|
uint64_t current_count = signal->get_count();
|
||||||
|
if (current_count + 3 < timestamp)
|
||||||
|
{
|
||||||
|
iface->latch_handles();
|
||||||
|
LOGI("Asset manager skipping iteration due to too much pending work.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskGroupHandle task;
|
||||||
|
if (group)
|
||||||
|
{
|
||||||
|
task = group->create_task();
|
||||||
|
task->set_desc("asset-manager-instantiate");
|
||||||
|
task->set_fence_counter_signal(signal.get());
|
||||||
|
task->set_task_class(TaskClass::Background);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
signal->signal_increment();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> holder{asset_bank_lock};
|
||||||
|
update_costs_locked_assets();
|
||||||
|
update_lru_locked_assets();
|
||||||
|
|
||||||
|
memcpy(sorted_assets.data(), asset_bank.data(), id_count * sizeof(sorted_assets[0]));
|
||||||
|
std::sort(sorted_assets.data(), sorted_assets.data() + id_count, [](const AssetInfo *a, const AssetInfo *b) -> bool {
|
||||||
|
// High prios come first since they will be activated.
|
||||||
|
// Then we sort by LRU.
|
||||||
|
// High consumption should be moved last, so they are candidates to be paged out if we're over budget.
|
||||||
|
// High pending consumption should be moved early since we don't want to page out resources that
|
||||||
|
// are in the middle of being loaded anyway.
|
||||||
|
// Finally, the ID is used as a tie breaker.
|
||||||
|
|
||||||
|
if (a->prio != b->prio)
|
||||||
|
return a->prio > b->prio;
|
||||||
|
else if (a->last_used != b->last_used)
|
||||||
|
return a->last_used > b->last_used;
|
||||||
|
else if (a->consumed != b->consumed)
|
||||||
|
return a->consumed < b->consumed;
|
||||||
|
else if (a->pending_consumed != b->pending_consumed)
|
||||||
|
return a->pending_consumed > b->pending_consumed;
|
||||||
|
else
|
||||||
|
return a->id.id < b->id.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
size_t release_index = id_count;
|
||||||
|
uint64_t activated_cost_this_iteration = 0;
|
||||||
|
unsigned activation_count = 0;
|
||||||
|
size_t activate_index = 0;
|
||||||
|
|
||||||
|
// Aim to activate resources as long as we're in budget.
|
||||||
|
// Activate in order from highest priority to lowest.
|
||||||
|
bool can_activate = true;
|
||||||
|
while (can_activate &&
|
||||||
|
total_consumed < transfer_budget &&
|
||||||
|
activated_cost_this_iteration < transfer_budget_per_iteration &&
|
||||||
|
activate_index != release_index)
|
||||||
|
{
|
||||||
|
auto *candidate = sorted_assets[activate_index];
|
||||||
|
if (candidate->prio <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// This resource is already active.
|
||||||
|
if (candidate->consumed != 0 || candidate->pending_consumed != 0)
|
||||||
|
{
|
||||||
|
activate_index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t estimate = iface->estimate_cost_asset(candidate->id, *candidate->handle);
|
||||||
|
|
||||||
|
can_activate = (total_consumed + estimate <= transfer_budget) || (candidate->prio >= persistent_prio());
|
||||||
|
while (!can_activate && activate_index + 1 != release_index)
|
||||||
|
{
|
||||||
|
auto *release_candidate = sorted_assets[--release_index];
|
||||||
|
if (release_candidate->consumed)
|
||||||
|
{
|
||||||
|
LOGI("Releasing ID %u due to page-in pressure.\n", release_candidate->id.id);
|
||||||
|
iface->release_asset(release_candidate->id);
|
||||||
|
total_consumed -= release_candidate->consumed;
|
||||||
|
release_candidate->consumed = 0;
|
||||||
|
}
|
||||||
|
can_activate = total_consumed + estimate <= transfer_budget;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (can_activate)
|
||||||
|
{
|
||||||
|
// We're trivially in budget.
|
||||||
|
iface->instantiate_asset(*this, task.get(), candidate->id, *candidate->handle);
|
||||||
|
activation_count++;
|
||||||
|
|
||||||
|
candidate->pending_consumed = estimate;
|
||||||
|
total_consumed += estimate;
|
||||||
|
// Let this run over budget once.
|
||||||
|
// Ensures we can make forward progress no matter what the limit is.
|
||||||
|
activated_cost_this_iteration += estimate;
|
||||||
|
activate_index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're 75% of budget, start garbage collecting non-resident resources ahead of time.
|
||||||
|
const uint64_t low_image_budget = (transfer_budget * 3) / 4;
|
||||||
|
|
||||||
|
const auto should_release = [&]() -> bool {
|
||||||
|
if (release_index == activate_index)
|
||||||
|
return false;
|
||||||
|
if (sorted_assets[release_index - 1]->prio == persistent_prio())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (total_consumed > transfer_budget)
|
||||||
|
return true;
|
||||||
|
else if (total_consumed > low_image_budget && sorted_assets[release_index - 1]->prio == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we're over budget, deactivate resources.
|
||||||
|
while (should_release())
|
||||||
|
{
|
||||||
|
auto *candidate = sorted_assets[--release_index];
|
||||||
|
if (candidate->consumed)
|
||||||
|
{
|
||||||
|
LOGI("Releasing 0-prio ID %u due to page-in pressure.\n", candidate->id.id);
|
||||||
|
iface->release_asset(candidate->id);
|
||||||
|
total_consumed -= candidate->consumed;
|
||||||
|
candidate->consumed = 0;
|
||||||
|
candidate->last_used = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activated_cost_this_iteration)
|
||||||
|
{
|
||||||
|
LOGI("Activated %u resources for %llu KiB.\n", activation_count,
|
||||||
|
static_cast<unsigned long long>(activated_cost_this_iteration / 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
iface->latch_handles();
|
||||||
|
timestamp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/* 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 "atomic_append_buffer.hpp"
|
||||||
|
#include "global_managers.hpp"
|
||||||
|
#include "filesystem.hpp"
|
||||||
|
#include "object_pool.hpp"
|
||||||
|
#include "intrusive_hash_map.hpp"
|
||||||
|
#include "dynamic_array.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <mutex>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
struct AssetID
|
||||||
|
{
|
||||||
|
uint32_t id = uint32_t(-1);
|
||||||
|
enum { MaxIDs = 1u << 18 };
|
||||||
|
AssetID() = default;
|
||||||
|
explicit AssetID(uint32_t id_) : id{id_} {}
|
||||||
|
explicit inline operator bool() const { return id != uint32_t(-1); }
|
||||||
|
inline bool operator==(const AssetID &other) const { return id == other.id; }
|
||||||
|
inline bool operator!=(const AssetID &other) const { return !(*this == other); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class AssetManager;
|
||||||
|
|
||||||
|
// If we have to fall back due to no image being present,
|
||||||
|
// lets asset instantiator know what to substitute.
|
||||||
|
enum class AssetClass
|
||||||
|
{
|
||||||
|
// Substitute with 0.
|
||||||
|
ImageZeroable,
|
||||||
|
// Substitute with missing color.
|
||||||
|
ImageColor,
|
||||||
|
// Substitute with RG8_UNORM 0.5
|
||||||
|
ImageNormal,
|
||||||
|
// Substitute with M = 0, R = 1.
|
||||||
|
ImageMetallicRoughness,
|
||||||
|
// Substitute with mid-gray (0.5, 0.5, 0.5, 1.0) UNORM8.
|
||||||
|
// Somewhat compatible with everything.
|
||||||
|
ImageGeneric,
|
||||||
|
Mesh
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThreadGroup;
|
||||||
|
struct TaskGroup;
|
||||||
|
struct TaskSignal;
|
||||||
|
|
||||||
|
class AssetInstantiatorInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~AssetInstantiatorInterface() = default;
|
||||||
|
|
||||||
|
// This estimate should be an upper bound.
|
||||||
|
virtual uint64_t estimate_cost_asset(AssetID id, File &mapping) = 0;
|
||||||
|
|
||||||
|
// When instantiation completes, manager.update_cost() must be called with the real cost.
|
||||||
|
// The real cost may only be known after async parsing of the file.
|
||||||
|
virtual void instantiate_asset(AssetManager &manager, TaskGroup *group, AssetID id, File &mapping) = 0;
|
||||||
|
|
||||||
|
// Will only be called after an upload completes through manager.update_cost().
|
||||||
|
virtual void release_asset(AssetID id) = 0;
|
||||||
|
|
||||||
|
virtual void set_id_bounds(uint32_t bound) = 0;
|
||||||
|
virtual void set_asset_class(AssetID id, AssetClass asset_class);
|
||||||
|
|
||||||
|
// Called in AssetManager::iterate().
|
||||||
|
virtual void latch_handles() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AssetManager final : public AssetManagerInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Persistent prio means the resource is treated as an internal LUT that must always be resident, no matter what.
|
||||||
|
constexpr static int persistent_prio() { return 0x7fffffff; }
|
||||||
|
|
||||||
|
AssetManager();
|
||||||
|
~AssetManager() override;
|
||||||
|
|
||||||
|
void set_asset_instantiator_interface(AssetInstantiatorInterface *iface);
|
||||||
|
|
||||||
|
// We might want to consider different budgets per asset class.
|
||||||
|
void set_asset_budget(uint64_t cost);
|
||||||
|
void set_asset_budget_per_iteration(uint64_t cost);
|
||||||
|
|
||||||
|
// FileHandle is intended to be used with FileSlice or similar here so that we don't need
|
||||||
|
// a ton of open files at once.
|
||||||
|
AssetID register_asset(FileHandle file, AssetClass asset_class, int prio = 1);
|
||||||
|
AssetID register_asset(Filesystem &fs, const std::string &path, AssetClass asset_class, int prio = 1);
|
||||||
|
|
||||||
|
// Prio 0: Not resident, resource may not exist.
|
||||||
|
bool set_asset_residency_priority(AssetID id, int prio);
|
||||||
|
|
||||||
|
// Intended to be called in Application::post_frame(). Not thread safe.
|
||||||
|
// This function updates internal state.
|
||||||
|
void iterate(ThreadGroup *group);
|
||||||
|
bool iterate_blocking(ThreadGroup &group, AssetID id);
|
||||||
|
|
||||||
|
// Always thread safe, used by AssetInstantiatorInterfaces to update cost estimates.
|
||||||
|
void update_cost(AssetID id, uint64_t cost);
|
||||||
|
|
||||||
|
// May be called concurrently, except when calling iterate().
|
||||||
|
uint64_t get_current_total_consumed() const;
|
||||||
|
|
||||||
|
// May be called concurrently, except when calling iterate().
|
||||||
|
// Intended to be called by asset instantiator interface or similar.
|
||||||
|
// When a resource is actually accessed, this is called.
|
||||||
|
void mark_used_asset(AssetID id);
|
||||||
|
|
||||||
|
// Should be called in applications's constructor to make sure we initialize
|
||||||
|
// the mesh asset pool on device creation.
|
||||||
|
// FIXME: Could be made more flexible if need be.
|
||||||
|
void enable_mesh_assets();
|
||||||
|
bool get_wants_mesh_assets() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct AssetInfo : Util::IntrusiveHashMapEnabled<AssetInfo>
|
||||||
|
{
|
||||||
|
uint64_t pending_consumed = 0;
|
||||||
|
uint64_t consumed = 0;
|
||||||
|
uint64_t last_used = 0;
|
||||||
|
FileHandle handle;
|
||||||
|
AssetID id = {};
|
||||||
|
AssetClass asset_class = AssetClass::ImageZeroable;
|
||||||
|
int prio = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Util::DynamicArray<AssetInfo *> sorted_assets;
|
||||||
|
Util::DynamicArray<AssetInfo *> asset_bank;
|
||||||
|
std::mutex asset_bank_lock;
|
||||||
|
Util::ObjectPool<AssetInfo> pool;
|
||||||
|
Util::AtomicAppendBuffer<AssetID> lru_append;
|
||||||
|
Util::IntrusiveHashMapHolder<AssetInfo> file_to_assets;
|
||||||
|
|
||||||
|
AssetInstantiatorInterface *iface = nullptr;
|
||||||
|
uint32_t id_count = 0;
|
||||||
|
uint64_t total_consumed = 0;
|
||||||
|
uint64_t transfer_budget = 0;
|
||||||
|
uint64_t transfer_budget_per_iteration = 0;
|
||||||
|
uint64_t timestamp = 1;
|
||||||
|
uint32_t blocking_signals = 0;
|
||||||
|
|
||||||
|
struct CostUpdate
|
||||||
|
{
|
||||||
|
AssetID id;
|
||||||
|
uint64_t cost = 0;
|
||||||
|
};
|
||||||
|
std::mutex cost_update_lock;
|
||||||
|
std::vector<CostUpdate> thread_cost_updates;
|
||||||
|
std::vector<CostUpdate> cost_updates;
|
||||||
|
|
||||||
|
void adjust_update(const CostUpdate &update);
|
||||||
|
std::unique_ptr<TaskSignal> signal;
|
||||||
|
AssetID register_asset_nolock(FileHandle file, AssetClass asset_class, int prio);
|
||||||
|
|
||||||
|
void update_costs_locked_assets();
|
||||||
|
void update_lru_locked_assets();
|
||||||
|
|
||||||
|
bool wants_mesh_assets = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,692 @@
|
|||||||
|
/* 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 "filesystem.hpp"
|
||||||
|
#include "path_utils.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include "os_filesystem.hpp"
|
||||||
|
#include "string_helpers.hpp"
|
||||||
|
#include "environment.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
std::vector<ListEntry> FilesystemBackend::walk(const std::string &path)
|
||||||
|
{
|
||||||
|
auto entries = list(path);
|
||||||
|
std::vector<ListEntry> final_entries;
|
||||||
|
for (auto &e : entries)
|
||||||
|
{
|
||||||
|
if (e.type == PathType::Directory)
|
||||||
|
{
|
||||||
|
auto subentries = walk(e.path);
|
||||||
|
final_entries.push_back(std::move(e));
|
||||||
|
for (auto &sub : subentries)
|
||||||
|
final_entries.push_back(std::move(sub));
|
||||||
|
}
|
||||||
|
else if (e.type == PathType::File)
|
||||||
|
final_entries.push_back(std::move(e));
|
||||||
|
}
|
||||||
|
return final_entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FilesystemBackend::remove(const std::string &)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FilesystemBackend::move_replace(const std::string &, const std::string &)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FilesystemBackend::move_yield(const std::string &, const std::string &)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Filesystem::Filesystem()
|
||||||
|
{
|
||||||
|
register_protocol("file", std::unique_ptr<FilesystemBackend>(new OSFilesystem(".")));
|
||||||
|
register_protocol("memory", std::unique_ptr<FilesystemBackend>(new ScratchFilesystem));
|
||||||
|
|
||||||
|
#ifdef GRANITE_DEFAULT_ASSET_DIRECTORY
|
||||||
|
auto asset_dir = Util::get_environment_string("GRANITE_DEFAULT_ASSET_DIRECTORY", GRANITE_DEFAULT_ASSET_DIRECTORY);
|
||||||
|
#else
|
||||||
|
auto asset_dir = Util::get_environment_string("GRANITE_DEFAULT_ASSET_DIRECTORY", "");
|
||||||
|
#endif
|
||||||
|
if (!asset_dir.empty())
|
||||||
|
register_protocol("builtin", std::unique_ptr<FilesystemBackend>(new OSFilesystem(asset_dir)));
|
||||||
|
|
||||||
|
#ifdef GRANITE_DEFAULT_BUILTIN_DIRECTORY
|
||||||
|
auto builtin_dir = Util::get_environment_string("GRANITE_DEFAULT_BUILTIN_DIRECTORY", GRANITE_DEFAULT_BUILTIN_DIRECTORY);
|
||||||
|
#else
|
||||||
|
auto builtin_dir = Util::get_environment_string("GRANITE_DEFAULT_BUILTIN_DIRECTORY", "");
|
||||||
|
#endif
|
||||||
|
if (!builtin_dir.empty())
|
||||||
|
register_protocol("builtin", std::unique_ptr<FilesystemBackend>(new OSFilesystem(builtin_dir)));
|
||||||
|
|
||||||
|
#ifdef GRANITE_DEFAULT_CACHE_DIRECTORY
|
||||||
|
auto cache_dir = Util::get_environment_string("GRANITE_DEFAULT_CACHE_DIRECTORY", GRANITE_DEFAULT_CACHE_DIRECTORY);
|
||||||
|
#else
|
||||||
|
auto cache_dir = Util::get_environment_string("GRANITE_DEFAULT_CACHE_DIRECTORY", "");
|
||||||
|
#endif
|
||||||
|
if (!cache_dir.empty())
|
||||||
|
register_protocol("cache", std::unique_ptr<FilesystemBackend>(new OSFilesystem(cache_dir)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Filesystem::setup_default_filesystem(Filesystem *filesystem, const char *default_asset_directory)
|
||||||
|
{
|
||||||
|
auto self_dir = Path::basedir(Path::get_executable_path());
|
||||||
|
auto assets_dir = Path::join(self_dir, "assets");
|
||||||
|
auto builtin_dir = Path::join(self_dir, "builtin/assets");
|
||||||
|
|
||||||
|
if (default_asset_directory)
|
||||||
|
{
|
||||||
|
#ifdef GRANITE_SHIPPING
|
||||||
|
LOGW("Default asset directory %s was provided, but this is only intended for non-shipping configs.\n",
|
||||||
|
default_asset_directory);
|
||||||
|
#else
|
||||||
|
filesystem->register_protocol("assets",
|
||||||
|
std::unique_ptr<FilesystemBackend>(new OSFilesystem(default_asset_directory)));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FileStat s;
|
||||||
|
if (filesystem->stat(assets_dir, s) && s.type == PathType::Directory)
|
||||||
|
{
|
||||||
|
filesystem->register_protocol("assets", std::make_unique<OSFilesystem>(assets_dir));
|
||||||
|
LOGI("Redirecting filesystem \"assets\" to %s.\n", assets_dir.c_str());
|
||||||
|
|
||||||
|
auto cache_dir = Path::join(self_dir, "cache");
|
||||||
|
filesystem->register_protocol("cache", std::make_unique<OSFilesystem>(cache_dir));
|
||||||
|
LOGI("Redirecting filesystem \"cache\" to %s.\n", cache_dir.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesystem->stat(builtin_dir, s) && s.type == PathType::Directory)
|
||||||
|
{
|
||||||
|
filesystem->register_protocol("builtin", std::make_unique<OSFilesystem>(builtin_dir));
|
||||||
|
LOGI("Redirecting filesystem \"builtin\" to %s.\n", builtin_dir.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// These filesystems are core functionality.
|
||||||
|
if (!filesystem->get_backend("builtin"))
|
||||||
|
throw std::runtime_error("builtin filesystem was not initialized.");
|
||||||
|
if (!filesystem->get_backend("cache"))
|
||||||
|
throw std::runtime_error("cache filesystem was not initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Filesystem::register_protocol(const std::string &proto, std::unique_ptr<FilesystemBackend> fs)
|
||||||
|
{
|
||||||
|
if (fs)
|
||||||
|
{
|
||||||
|
fs->set_protocol(proto);
|
||||||
|
protocols[proto] = std::move(fs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
protocols.erase(proto);
|
||||||
|
}
|
||||||
|
|
||||||
|
FilesystemBackend *Filesystem::get_backend(const std::string &proto)
|
||||||
|
{
|
||||||
|
auto itr = protocols.find(proto);
|
||||||
|
if (proto.empty())
|
||||||
|
itr = protocols.find("file");
|
||||||
|
|
||||||
|
if (itr != end(protocols))
|
||||||
|
return itr->second.get();
|
||||||
|
else
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListEntry> Filesystem::walk(const std::string &path)
|
||||||
|
{
|
||||||
|
auto paths = Path::protocol_split(path);
|
||||||
|
auto *backend = get_backend(paths.first);
|
||||||
|
if (!backend)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return backend->walk(paths.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListEntry> Filesystem::list(const std::string &path)
|
||||||
|
{
|
||||||
|
auto paths = Path::protocol_split(path);
|
||||||
|
auto *backend = get_backend(paths.first);
|
||||||
|
if (!backend)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return backend->list(paths.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Filesystem::remove(const std::string &path)
|
||||||
|
{
|
||||||
|
auto paths = Path::protocol_split(path);
|
||||||
|
auto *backend = get_backend(paths.first);
|
||||||
|
if (!backend)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return backend->remove(paths.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Filesystem::move_yield(const std::string &dst, const std::string &src)
|
||||||
|
{
|
||||||
|
auto paths_dst = Path::protocol_split(dst);
|
||||||
|
auto paths_src = Path::protocol_split(src);
|
||||||
|
auto *backend_dst = get_backend(paths_dst.first);
|
||||||
|
auto *backend_src = get_backend(paths_src.first);
|
||||||
|
if (!backend_dst || !backend_src || backend_dst != backend_src)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return backend_dst->move_yield(paths_dst.second, paths_src.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Filesystem::move_replace(const std::string &dst, const std::string &src)
|
||||||
|
{
|
||||||
|
auto paths_dst = Path::protocol_split(dst);
|
||||||
|
auto paths_src = Path::protocol_split(src);
|
||||||
|
auto *backend_dst = get_backend(paths_dst.first);
|
||||||
|
auto *backend_src = get_backend(paths_src.first);
|
||||||
|
if (!backend_dst || !backend_src || backend_dst != backend_src)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return backend_dst->move_replace(paths_dst.second, paths_src.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle Filesystem::open_readonly_mapping(const std::string &path)
|
||||||
|
{
|
||||||
|
auto file = open(path, FileMode::ReadOnly);
|
||||||
|
if (!file)
|
||||||
|
return {};
|
||||||
|
return file->map();
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle Filesystem::open_writeonly_mapping(const std::string &path, size_t size)
|
||||||
|
{
|
||||||
|
auto file = open(path, FileMode::WriteOnly);
|
||||||
|
if (!file)
|
||||||
|
return {};
|
||||||
|
return file->map_write(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle Filesystem::open_transactional_mapping(const std::string &path, size_t size)
|
||||||
|
{
|
||||||
|
auto file = open(path, FileMode::WriteOnlyTransactional);
|
||||||
|
if (!file)
|
||||||
|
return {};
|
||||||
|
return file->map_write(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Filesystem::read_file_to_string(const std::string &path, std::string &str)
|
||||||
|
{
|
||||||
|
auto mapping = open_readonly_mapping(path);
|
||||||
|
if (!mapping)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto size = mapping->get_size();
|
||||||
|
str = std::string(mapping->data<char>(), mapping->data<char>() + size);
|
||||||
|
|
||||||
|
// Remove DOS EOL.
|
||||||
|
str.erase(remove_if(begin(str), end(str), [](char c) { return c == '\r'; }), end(str));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Filesystem::write_buffer_to_file(const std::string &path, const void *data, size_t size)
|
||||||
|
{
|
||||||
|
auto file = open_transactional_mapping(path, size);
|
||||||
|
if (!file)
|
||||||
|
return false;
|
||||||
|
memcpy(file->mutable_data(), data, size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Filesystem::write_string_to_file(const std::string &path, const std::string &str)
|
||||||
|
{
|
||||||
|
return write_buffer_to_file(path, str.data(), str.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHandle Filesystem::open(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
auto paths = Path::protocol_split(path);
|
||||||
|
auto *backend = get_backend(paths.first);
|
||||||
|
if (!backend)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
auto file = backend->open(paths.second, mode);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Filesystem::get_filesystem_path(const std::string &path)
|
||||||
|
{
|
||||||
|
auto paths = Path::protocol_split(path);
|
||||||
|
auto *backend = get_backend(paths.first);
|
||||||
|
if (!backend)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return backend->get_filesystem_path(paths.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Filesystem::stat(const std::string &path, FileStat &stat)
|
||||||
|
{
|
||||||
|
auto paths = Path::protocol_split(path);
|
||||||
|
auto *backend = get_backend(paths.first);
|
||||||
|
if (!backend)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return backend->stat(paths.second, stat);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Filesystem::poll_notifications()
|
||||||
|
{
|
||||||
|
for (auto &proto : protocols)
|
||||||
|
proto.second->poll_notifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Filesystem::load_text_file(const std::string &path, std::string &str)
|
||||||
|
{
|
||||||
|
return read_file_to_string(path, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScratchFilesystem::get_notification_fd() const
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileNotifyHandle ScratchFilesystem::install_notification(const std::string &,
|
||||||
|
std::function<void(const FileNotifyInfo &)>)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScratchFilesystem::poll_notifications()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScratchFilesystem::uninstall_notification(FileNotifyHandle)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScratchFilesystem::stat(const std::string &path, FileStat &stat)
|
||||||
|
{
|
||||||
|
auto itr = scratch_files.find(path);
|
||||||
|
if (itr == end(scratch_files))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
stat.size = itr->second->data.size();
|
||||||
|
stat.type = PathType::File;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListEntry> ScratchFilesystem::list(const std::string &)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScratchFilesystemFile final : File
|
||||||
|
{
|
||||||
|
explicit ScratchFilesystemFile(std::vector<uint8_t> &data_)
|
||||||
|
: data(data_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle map_subset(uint64_t offset, size_t range) override
|
||||||
|
{
|
||||||
|
if (offset + range > data.size())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return Util::make_handle<FileMapping>(
|
||||||
|
FileHandle{}, offset,
|
||||||
|
data.data() + offset, range,
|
||||||
|
0, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle map_write(size_t size) override
|
||||||
|
{
|
||||||
|
data.resize(size);
|
||||||
|
return map_subset(0, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unmap(void *, size_t) override
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t get_size() override
|
||||||
|
{
|
||||||
|
return data.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> &data;
|
||||||
|
};
|
||||||
|
|
||||||
|
FileHandle ScratchFilesystem::open(const std::string &path, FileMode)
|
||||||
|
{
|
||||||
|
auto itr = scratch_files.find(path);
|
||||||
|
if (itr == end(scratch_files))
|
||||||
|
{
|
||||||
|
auto &file = scratch_files[path];
|
||||||
|
file = std::make_unique<ScratchFile>();
|
||||||
|
return Util::make_handle<ScratchFilesystemFile>(file->data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Util::make_handle<ScratchFilesystemFile>(itr->second->data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobFilesystem::BlobFilesystem(FileHandle file_)
|
||||||
|
: file(std::move(file_))
|
||||||
|
{
|
||||||
|
if (!file)
|
||||||
|
return;
|
||||||
|
|
||||||
|
root = std::make_unique<Directory>();
|
||||||
|
parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t BlobFilesystem::read_u8(const uint8_t *&buf, size_t &size)
|
||||||
|
{
|
||||||
|
if (size < 1)
|
||||||
|
throw std::range_error("Blob EOF.");
|
||||||
|
|
||||||
|
uint8_t ret = *buf++;
|
||||||
|
size--;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t BlobFilesystem::read_u64(const uint8_t *&buf, size_t &size)
|
||||||
|
{
|
||||||
|
if (size < 8)
|
||||||
|
throw std::range_error("Blob EOF.");
|
||||||
|
|
||||||
|
uint64_t ret = 0;
|
||||||
|
for (unsigned i = 0; i < 8; i++)
|
||||||
|
ret |= uint64_t(buf[i]) << (8 * i);
|
||||||
|
size -= 8;
|
||||||
|
buf += 8;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string BlobFilesystem::read_string(const uint8_t *&buf, size_t &size, size_t len)
|
||||||
|
{
|
||||||
|
if (size < len)
|
||||||
|
throw std::range_error("Blob EOF.");
|
||||||
|
|
||||||
|
std::string ret;
|
||||||
|
ret.insert(ret.end(), reinterpret_cast<const char *>(buf), reinterpret_cast<const char *>(buf) + len);
|
||||||
|
size -= len;
|
||||||
|
buf += len;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobFilesystem::add_entry(const std::string &path, size_t offset, size_t size)
|
||||||
|
{
|
||||||
|
auto paths = Path::split(path);
|
||||||
|
auto *dir = find_directory(paths.first);
|
||||||
|
if (!dir)
|
||||||
|
dir = make_directory(paths.first);
|
||||||
|
dir->files.push_back({ Path::basename(path), offset, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobFilesystem::parse()
|
||||||
|
{
|
||||||
|
size_t mapped_size = file->get_size();
|
||||||
|
if (mapped_size < 16)
|
||||||
|
throw std::runtime_error("Blob archive too small.");
|
||||||
|
|
||||||
|
auto mapped_handle = file->map();
|
||||||
|
if (!mapped_handle)
|
||||||
|
throw std::runtime_error("Failed to map blob archive.");
|
||||||
|
|
||||||
|
auto *base_mapped = mapped_handle->data<uint8_t>();
|
||||||
|
auto *mapped = base_mapped;
|
||||||
|
|
||||||
|
if (memcmp(mapped, "BLOBBY01", 8) != 0)
|
||||||
|
throw std::runtime_error("Invalid magic.");
|
||||||
|
mapped += 8;
|
||||||
|
mapped_size -= 8;
|
||||||
|
|
||||||
|
uint64_t required_size = 0;
|
||||||
|
|
||||||
|
while (mapped_size >= 4 && memcmp(mapped, "ENTR", 4) == 0)
|
||||||
|
{
|
||||||
|
mapped += 4;
|
||||||
|
mapped_size -= 4;
|
||||||
|
|
||||||
|
uint8_t len = read_u8(mapped, mapped_size);
|
||||||
|
std::string path = Path::canonicalize_path(read_string(mapped, mapped_size, len));
|
||||||
|
uint64_t blob_offset = read_u64(mapped, mapped_size);
|
||||||
|
uint64_t blob_size = read_u64(mapped, mapped_size);
|
||||||
|
required_size = std::max(required_size, blob_offset + blob_size);
|
||||||
|
|
||||||
|
if (blob_offset + blob_size < blob_offset)
|
||||||
|
throw std::range_error("Overflow for blob offset + size.");
|
||||||
|
|
||||||
|
if (blob_offset > SIZE_MAX || blob_size > SIZE_MAX)
|
||||||
|
throw std::range_error("Blob offset out of range.");
|
||||||
|
|
||||||
|
add_entry(path, blob_offset, blob_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapped_size >= 4 && memcmp(mapped, "DATA", 4) == 0)
|
||||||
|
{
|
||||||
|
blob_base_offset = size_t((mapped + 4) - base_mapped);
|
||||||
|
mapped_size -= 4;
|
||||||
|
if (mapped_size < required_size)
|
||||||
|
throw std::range_error("Blob is not large enough for all files.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobFilesystem::Directory *BlobFilesystem::make_directory(const std::string &path)
|
||||||
|
{
|
||||||
|
auto split = Util::split_no_empty(path, "/");
|
||||||
|
auto *dir = root.get();
|
||||||
|
|
||||||
|
for (const auto &subpath : split)
|
||||||
|
{
|
||||||
|
auto dir_itr = std::find_if(dir->dirs.begin(), dir->dirs.end(), [&](const std::unique_ptr<Directory> &dir_) {
|
||||||
|
return subpath == dir_->path;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dir_itr != dir->dirs.end())
|
||||||
|
dir = dir_itr->get();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dir->dirs.emplace_back(new Directory);
|
||||||
|
dir->dirs.back()->path = subpath;
|
||||||
|
dir = dir->dirs.back().get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobFilesystem::Directory *BlobFilesystem::find_directory(const std::string &path)
|
||||||
|
{
|
||||||
|
auto split = Util::split_no_empty(path, "/");
|
||||||
|
auto *dir = root.get();
|
||||||
|
|
||||||
|
for (const auto &subpath : split)
|
||||||
|
{
|
||||||
|
auto dir_itr = std::find_if(dir->dirs.begin(), dir->dirs.end(), [&](const std::unique_ptr<Directory> &dir_) {
|
||||||
|
return subpath == dir_->path;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dir_itr != dir->dirs.end())
|
||||||
|
dir = dir_itr->get();
|
||||||
|
else
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlobFilesystem::BlobFile *BlobFilesystem::find_file(const std::string &path)
|
||||||
|
{
|
||||||
|
auto paths = Path::split(path);
|
||||||
|
auto *dir = find_directory(paths.first);
|
||||||
|
if (!dir)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto file_itr = std::find_if(dir->files.begin(), dir->files.end(), [&](const BlobFile &zip_file) {
|
||||||
|
return paths.second == zip_file.path;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file_itr != dir->files.end())
|
||||||
|
return &*file_itr;
|
||||||
|
else
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListEntry> BlobFilesystem::list(const std::string &path)
|
||||||
|
{
|
||||||
|
auto canon_path = Path::canonicalize_path(path);
|
||||||
|
|
||||||
|
std::vector<ListEntry> entries;
|
||||||
|
if (const auto *zip_dir = find_directory(canon_path))
|
||||||
|
{
|
||||||
|
entries.reserve(zip_dir->dirs.size() + zip_dir->files.size());
|
||||||
|
for (auto &dir : zip_dir->dirs)
|
||||||
|
entries.push_back({ Path::join(path, dir->path), PathType::Directory });
|
||||||
|
for (auto &f : zip_dir->files)
|
||||||
|
entries.push_back({ Path::join(path, f.path), PathType::File });
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BlobFilesystem::stat(const std::string &path, FileStat &stat)
|
||||||
|
{
|
||||||
|
auto p = Path::canonicalize_path(path);
|
||||||
|
|
||||||
|
if (const auto *zip_file = find_file(p))
|
||||||
|
{
|
||||||
|
stat.size = zip_file->size;
|
||||||
|
stat.type = PathType::File;
|
||||||
|
stat.last_modified = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (find_directory(p))
|
||||||
|
{
|
||||||
|
stat.size = 0;
|
||||||
|
stat.last_modified = 0;
|
||||||
|
stat.type = PathType::Directory;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHandle BlobFilesystem::open(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
if (mode != FileMode::ReadOnly)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
auto p = Path::canonicalize_path(path);
|
||||||
|
auto *blob_file = find_file(p);
|
||||||
|
if (!blob_file)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return Util::make_handle<FileSlice>(file, blob_base_offset + blob_file->offset, blob_file->size);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileNotifyHandle BlobFilesystem::install_notification(const std::string &, std::function<void (const FileNotifyInfo &)>)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobFilesystem::uninstall_notification(FileNotifyHandle)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlobFilesystem::poll_notifications()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int BlobFilesystem::get_notification_fd() const
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMapping::FileMapping(FileHandle handle_, uint64_t file_offset_,
|
||||||
|
void *mapped_, size_t mapped_size_,
|
||||||
|
size_t map_offset_, size_t accessible_size_)
|
||||||
|
: handle(std::move(handle_))
|
||||||
|
, file_offset(file_offset_)
|
||||||
|
, mapped(mapped_)
|
||||||
|
, mapped_size(mapped_size_)
|
||||||
|
, map_offset(map_offset_)
|
||||||
|
, accessible_size(accessible_size_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMapping::~FileMapping()
|
||||||
|
{
|
||||||
|
if (handle)
|
||||||
|
handle->unmap(mapped, mapped_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t FileMapping::get_file_offset() const
|
||||||
|
{
|
||||||
|
return file_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t FileMapping::get_size() const
|
||||||
|
{
|
||||||
|
return accessible_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Util::IntrusivePtr<FileMapping> File::map()
|
||||||
|
{
|
||||||
|
return map_subset(0, get_size());
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSlice::FileSlice(FileHandle handle_, uint64_t offset_, uint64_t range_)
|
||||||
|
: handle(std::move(handle_)), offset(offset_), range(range_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle FileSlice::map_subset(uint64_t offset_, size_t range_)
|
||||||
|
{
|
||||||
|
if (offset_ + range_ > range)
|
||||||
|
return {};
|
||||||
|
return handle->map_subset(offset + offset_, range_);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle FileSlice::map_write(size_t)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t FileSlice::get_size()
|
||||||
|
{
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileSlice::unmap(void *mapped, size_t mapped_size)
|
||||||
|
{
|
||||||
|
handle->unmap(mapped, mapped_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
/* 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 <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <functional>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "global_managers.hpp"
|
||||||
|
#include "intrusive.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class FileMapping;
|
||||||
|
|
||||||
|
class File : public Util::ThreadSafeIntrusivePtrEnabled<File>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~File() = default;
|
||||||
|
virtual Util::IntrusivePtr<FileMapping> map_subset(uint64_t offset, size_t range) = 0;
|
||||||
|
virtual Util::IntrusivePtr<FileMapping> map_write(size_t size) = 0;
|
||||||
|
virtual uint64_t get_size() = 0;
|
||||||
|
|
||||||
|
// Only called by FileMapping.
|
||||||
|
virtual void unmap(void *mapped, size_t range) = 0;
|
||||||
|
|
||||||
|
Util::IntrusivePtr<FileMapping> map();
|
||||||
|
};
|
||||||
|
using FileHandle = Util::IntrusivePtr<File>;
|
||||||
|
|
||||||
|
class FileMapping : public Util::ThreadSafeIntrusivePtrEnabled<FileMapping>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
template <typename T = void>
|
||||||
|
inline const T *data() const
|
||||||
|
{
|
||||||
|
void *ptr = static_cast<uint8_t *>(mapped) + map_offset;
|
||||||
|
return static_cast<const T *>(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T = void>
|
||||||
|
inline T *mutable_data()
|
||||||
|
{
|
||||||
|
void *ptr = static_cast<uint8_t *>(mapped) + map_offset;
|
||||||
|
return static_cast<T *>(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t get_file_offset() const;
|
||||||
|
uint64_t get_size() const;
|
||||||
|
|
||||||
|
~FileMapping();
|
||||||
|
FileMapping(FileHandle handle,
|
||||||
|
uint64_t file_offset,
|
||||||
|
void *mapped, size_t mapped_size,
|
||||||
|
size_t map_offset, size_t accessible_size);
|
||||||
|
|
||||||
|
private:
|
||||||
|
FileHandle handle;
|
||||||
|
uint64_t file_offset;
|
||||||
|
void *mapped;
|
||||||
|
size_t mapped_size;
|
||||||
|
// For non-page aligned maps.
|
||||||
|
size_t map_offset;
|
||||||
|
size_t accessible_size;
|
||||||
|
};
|
||||||
|
using FileMappingHandle = Util::IntrusivePtr<FileMapping>;
|
||||||
|
|
||||||
|
enum class PathType
|
||||||
|
{
|
||||||
|
File,
|
||||||
|
Directory,
|
||||||
|
Special
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ListEntry
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
PathType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileStat
|
||||||
|
{
|
||||||
|
uint64_t size;
|
||||||
|
PathType type;
|
||||||
|
uint64_t last_modified;
|
||||||
|
};
|
||||||
|
|
||||||
|
using FileNotifyHandle = int;
|
||||||
|
|
||||||
|
enum class FileNotifyType
|
||||||
|
{
|
||||||
|
FileChanged,
|
||||||
|
FileDeleted,
|
||||||
|
FileCreated,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileNotifyInfo
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
FileNotifyType type;
|
||||||
|
FileNotifyHandle handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class FileMode
|
||||||
|
{
|
||||||
|
ReadOnly,
|
||||||
|
WriteOnly,
|
||||||
|
ReadWrite,
|
||||||
|
WriteOnlyTransactional
|
||||||
|
};
|
||||||
|
|
||||||
|
class FilesystemBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~FilesystemBackend() = default;
|
||||||
|
std::vector<ListEntry> walk(const std::string &path);
|
||||||
|
virtual std::vector<ListEntry> list(const std::string &path) = 0;
|
||||||
|
virtual FileHandle open(const std::string &path, FileMode mode = FileMode::ReadOnly) = 0;
|
||||||
|
virtual bool stat(const std::string &path, FileStat &stat) = 0;
|
||||||
|
|
||||||
|
virtual FileNotifyHandle
|
||||||
|
install_notification(const std::string &path, std::function<void(const FileNotifyInfo &)> func) = 0;
|
||||||
|
|
||||||
|
virtual void uninstall_notification(FileNotifyHandle handle) = 0;
|
||||||
|
virtual void poll_notifications() = 0;
|
||||||
|
virtual int get_notification_fd() const = 0;
|
||||||
|
|
||||||
|
virtual bool remove(const std::string &path);
|
||||||
|
virtual bool move_replace(const std::string &dst, const std::string &src);
|
||||||
|
virtual bool move_yield(const std::string &dst, const std::string &src);
|
||||||
|
|
||||||
|
inline virtual std::string get_filesystem_path(const std::string &)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_protocol(const std::string &proto)
|
||||||
|
{
|
||||||
|
protocol = proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string protocol;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Filesystem final : public FilesystemInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Filesystem();
|
||||||
|
|
||||||
|
void register_protocol(const std::string &proto, std::unique_ptr<FilesystemBackend> fs);
|
||||||
|
FilesystemBackend *get_backend(const std::string &proto);
|
||||||
|
std::vector<ListEntry> walk(const std::string &path);
|
||||||
|
std::vector<ListEntry> list(const std::string &path);
|
||||||
|
FileHandle open(const std::string &path, FileMode mode = FileMode::ReadOnly);
|
||||||
|
std::string get_filesystem_path(const std::string &path);
|
||||||
|
|
||||||
|
bool read_file_to_string(const std::string &path, std::string &str);
|
||||||
|
bool write_string_to_file(const std::string &path, const std::string &str);
|
||||||
|
bool write_buffer_to_file(const std::string &path, const void *data, size_t size);
|
||||||
|
FileMappingHandle open_readonly_mapping(const std::string &path);
|
||||||
|
FileMappingHandle open_writeonly_mapping(const std::string &path, size_t size);
|
||||||
|
FileMappingHandle open_transactional_mapping(const std::string &path, size_t size);
|
||||||
|
|
||||||
|
bool remove(const std::string &path);
|
||||||
|
bool move_replace(const std::string &dst, const std::string &src);
|
||||||
|
bool move_yield(const std::string &dst, const std::string &src);
|
||||||
|
|
||||||
|
bool stat(const std::string &path, FileStat &stat);
|
||||||
|
|
||||||
|
void poll_notifications();
|
||||||
|
|
||||||
|
const std::unordered_map<std::string, std::unique_ptr<FilesystemBackend>> &get_protocols() const
|
||||||
|
{
|
||||||
|
return protocols;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_default_filesystem(Filesystem *fs, const char *default_asset_directory);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<FilesystemBackend>> protocols;
|
||||||
|
|
||||||
|
bool load_text_file(const std::string &path, std::string &str) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ScratchFilesystem final : public FilesystemBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::vector<ListEntry> list(const std::string &path) override;
|
||||||
|
|
||||||
|
FileHandle open(const std::string &path, FileMode mode = FileMode::ReadOnly) override;
|
||||||
|
|
||||||
|
bool stat(const std::string &path, FileStat &stat) override;
|
||||||
|
|
||||||
|
FileNotifyHandle install_notification(const std::string &path, std::function<void(const FileNotifyInfo &)> func) override;
|
||||||
|
|
||||||
|
void uninstall_notification(FileNotifyHandle handle) override;
|
||||||
|
|
||||||
|
void poll_notifications() override;
|
||||||
|
|
||||||
|
int get_notification_fd() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ScratchFile
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<ScratchFile>> scratch_files;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ConstantMemoryFile final : public Granite::File
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ConstantMemoryFile(const void *mapped_, size_t size_)
|
||||||
|
: mapped(static_cast<const uint8_t *>(mapped_)), size(size_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle map_subset(uint64_t offset, size_t range) override
|
||||||
|
{
|
||||||
|
if (offset + range > size)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return Util::make_handle<FileMapping>(
|
||||||
|
FileHandle{}, offset,
|
||||||
|
const_cast<uint8_t *>(mapped) + offset, range,
|
||||||
|
0, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle map_write(size_t) override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void unmap(void *, size_t) override
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t get_size() override
|
||||||
|
{
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const uint8_t *mapped;
|
||||||
|
size_t size;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileSlice final : public File
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FileSlice(FileHandle handle, uint64_t offset, uint64_t range);
|
||||||
|
FileMappingHandle map_subset(uint64_t offset, size_t range) override;
|
||||||
|
FileMappingHandle map_write(size_t) override;
|
||||||
|
void unmap(void *, size_t) override;
|
||||||
|
uint64_t get_size() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FileHandle handle;
|
||||||
|
uint64_t offset;
|
||||||
|
uint64_t range;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BlobFilesystem final : public FilesystemBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
BlobFilesystem(FileHandle file);
|
||||||
|
|
||||||
|
std::vector<ListEntry> list(const std::string &path) override;
|
||||||
|
|
||||||
|
FileHandle open(const std::string &path, FileMode mode) override;
|
||||||
|
|
||||||
|
bool stat(const std::string &path, FileStat &stat) override;
|
||||||
|
|
||||||
|
FileNotifyHandle install_notification(const std::string &path, std::function<void(const FileNotifyInfo &)> func) override;
|
||||||
|
|
||||||
|
void uninstall_notification(FileNotifyHandle handle) override;
|
||||||
|
|
||||||
|
void poll_notifications() override;
|
||||||
|
|
||||||
|
int get_notification_fd() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FileHandle file;
|
||||||
|
size_t blob_base_offset = 0;
|
||||||
|
|
||||||
|
struct BlobFile
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
size_t offset;
|
||||||
|
size_t size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Directory
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
std::vector<std::unique_ptr<Directory>> dirs;
|
||||||
|
std::vector<BlobFile> files;
|
||||||
|
};
|
||||||
|
std::unique_ptr<Directory> root;
|
||||||
|
BlobFile *find_file(const std::string &path);
|
||||||
|
Directory *find_directory(const std::string &path);
|
||||||
|
Directory *make_directory(const std::string &path);
|
||||||
|
void parse();
|
||||||
|
|
||||||
|
static uint8_t read_u8(const uint8_t *&buf, size_t &size);
|
||||||
|
static uint64_t read_u64(const uint8_t *&buf, size_t &size);
|
||||||
|
static std::string read_string(const uint8_t *&buf, size_t &size, size_t len);
|
||||||
|
void add_entry(const std::string &path, size_t offset, size_t size);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
+541
@@ -0,0 +1,541 @@
|
|||||||
|
/* 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 "os_filesystem.hpp"
|
||||||
|
#include "path_utils.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <atomic>
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <sys/inotify.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(__FreeBSD__) || defined(__APPLE__)
|
||||||
|
#define FSTAT64 fstat
|
||||||
|
#define FTRUNCATE64 ftruncate
|
||||||
|
#define MMAP64 mmap
|
||||||
|
#define STAT64 stat
|
||||||
|
#define off64_t off_t
|
||||||
|
#else
|
||||||
|
#define FSTAT64 fstat64
|
||||||
|
#define FTRUNCATE64 ftruncate64
|
||||||
|
#define MMAP64 mmap64
|
||||||
|
#define STAT64 stat64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
static bool ensure_directory_inner(const std::string &path)
|
||||||
|
{
|
||||||
|
if (Path::is_root_path(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
struct STAT64 s = {};
|
||||||
|
if (::STAT64(path.c_str(), &s) >= 0 && S_ISDIR(s.st_mode))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
auto basedir = Path::basedir(path);
|
||||||
|
if (!ensure_directory_inner(basedir))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (mkdir(path.c_str(), 0750) >= 0) || (errno == EEXIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ensure_directory(const std::string &path)
|
||||||
|
{
|
||||||
|
auto basedir = Path::basedir(path);
|
||||||
|
return ensure_directory_inner(basedir);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHandle MMapFile::open(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
auto file = Util::make_handle<MMapFile>();
|
||||||
|
if (!file->init(path, mode))
|
||||||
|
file.reset();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic_uint32_t global_transaction_counter;
|
||||||
|
|
||||||
|
bool MMapFile::init(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
int modeflags = 0;
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case FileMode::ReadOnly:
|
||||||
|
modeflags = O_RDONLY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileMode::WriteOnly:
|
||||||
|
case FileMode::WriteOnlyTransactional:
|
||||||
|
if (!ensure_directory(path))
|
||||||
|
{
|
||||||
|
LOGE("MMapFile failed to create directory.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
modeflags = O_RDWR | O_CREAT | O_TRUNC; // Need read access for mmap.
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileMode::ReadWrite:
|
||||||
|
if (!ensure_directory(path))
|
||||||
|
{
|
||||||
|
LOGE("MMapFile failed to create directory.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
modeflags = O_RDWR | O_CREAT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *open_path = path.c_str();
|
||||||
|
|
||||||
|
if (mode == FileMode::WriteOnlyTransactional)
|
||||||
|
{
|
||||||
|
// Use atomic file rename to ensure that a file is written atomically.
|
||||||
|
rename_to_on_close = path;
|
||||||
|
rename_from_on_close =
|
||||||
|
path + ".tmp." +
|
||||||
|
std::to_string(getpid()) + "." +
|
||||||
|
std::to_string(global_transaction_counter.fetch_add(1, std::memory_order_relaxed));
|
||||||
|
open_path = rename_from_on_close.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
fd = ::open(open_path, modeflags, 0640);
|
||||||
|
if (fd < 0)
|
||||||
|
{
|
||||||
|
rename_to_on_close.clear();
|
||||||
|
rename_from_on_close.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query_stat())
|
||||||
|
{
|
||||||
|
close(fd);
|
||||||
|
rename_to_on_close.clear();
|
||||||
|
rename_from_on_close.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle MMapFile::map_write(size_t map_size)
|
||||||
|
{
|
||||||
|
if (has_write_map)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (FTRUNCATE64(fd, off64_t(map_size)) < 0)
|
||||||
|
{
|
||||||
|
LOGE("Failed to truncate.\n");
|
||||||
|
report_error();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
size = map_size;
|
||||||
|
|
||||||
|
void *mapped = MMAP64(nullptr, map_size, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);
|
||||||
|
if (mapped == MAP_FAILED)
|
||||||
|
{
|
||||||
|
report_error();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
has_write_map = true;
|
||||||
|
|
||||||
|
return Util::make_handle<FileMapping>(
|
||||||
|
reference_from_this(),
|
||||||
|
0,
|
||||||
|
mapped, map_size,
|
||||||
|
0, map_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MMapFile::report_error()
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
int err = errno;
|
||||||
|
char fdpath[PATH_MAX];
|
||||||
|
char path[PATH_MAX];
|
||||||
|
snprintf(fdpath, sizeof(fdpath), "/proc/%u/fd/%d", getpid(), fd);
|
||||||
|
int ret = readlink(fdpath, path, sizeof(path) - 1);
|
||||||
|
if (ret > 0)
|
||||||
|
{
|
||||||
|
path[ret] = '\0';
|
||||||
|
LOGE("mmap failed for \"%s\" (%s).\n", path, strerror(err));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle MMapFile::map_subset(uint64_t offset, size_t range)
|
||||||
|
{
|
||||||
|
uint64_t page_size = sysconf(_SC_PAGESIZE);
|
||||||
|
uint64_t begin_map = offset & ~(page_size - 1);
|
||||||
|
uint64_t end_map = offset + range;
|
||||||
|
size_t mapped_size = end_map - begin_map;
|
||||||
|
|
||||||
|
// length need not be aligned.
|
||||||
|
|
||||||
|
void *mapped = MMAP64(nullptr, mapped_size, PROT_READ, MAP_PRIVATE, fd, off64_t(begin_map));
|
||||||
|
if (mapped == MAP_FAILED)
|
||||||
|
{
|
||||||
|
report_error();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Util::make_handle<FileMapping>(
|
||||||
|
reference_from_this(),
|
||||||
|
offset,
|
||||||
|
mapped, mapped_size,
|
||||||
|
offset - begin_map, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t MMapFile::get_size()
|
||||||
|
{
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MMapFile::query_stat()
|
||||||
|
{
|
||||||
|
struct STAT64 s = {};
|
||||||
|
if (FSTAT64(fd, &s) < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (uint64_t(s.st_size) > SIZE_MAX)
|
||||||
|
return false;
|
||||||
|
size = static_cast<size_t>(s.st_size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MMapFile::unmap(void *mapped, size_t mapped_size)
|
||||||
|
{
|
||||||
|
munmap(mapped, mapped_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
MMapFile::~MMapFile()
|
||||||
|
{
|
||||||
|
if (fd >= 0)
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
if (!rename_from_on_close.empty() && !rename_to_on_close.empty())
|
||||||
|
{
|
||||||
|
int ret = rename(rename_from_on_close.c_str(), rename_to_on_close.c_str());
|
||||||
|
if (ret != 0)
|
||||||
|
LOGE("Failed to rename file %s -> %s.\n", rename_from_on_close.c_str(), rename_to_on_close.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OSFilesystem::OSFilesystem(const std::string &base_)
|
||||||
|
: base(base_)
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
notify_fd = inotify_init1(IN_NONBLOCK);
|
||||||
|
if (notify_fd < 0)
|
||||||
|
LOGE("Failed to init inotify.\n");
|
||||||
|
#else
|
||||||
|
notify_fd = -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
OSFilesystem::~OSFilesystem()
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
if (notify_fd > 0)
|
||||||
|
{
|
||||||
|
for (auto &handler : handlers)
|
||||||
|
inotify_rm_watch(notify_fd, handler.first);
|
||||||
|
close(notify_fd);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHandle OSFilesystem::open(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
return MMapFile::open(Path::join(base, path), mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OSFilesystem::get_filesystem_path(const std::string &path)
|
||||||
|
{
|
||||||
|
return Path::join(base, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
int OSFilesystem::get_notification_fd() const
|
||||||
|
{
|
||||||
|
return notify_fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OSFilesystem::poll_notifications()
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
if (notify_fd < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
alignas(inotify_event) char buffer[sizeof(inotify_event) + NAME_MAX + 1];
|
||||||
|
ssize_t ret = read(notify_fd, buffer, sizeof(buffer));
|
||||||
|
|
||||||
|
if (ret < 0)
|
||||||
|
{
|
||||||
|
if (errno != EAGAIN)
|
||||||
|
LOGE("failed to read inotify fd.\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct inotify_event *current = nullptr;
|
||||||
|
for (ssize_t i = 0; i < ret; i += current->len + sizeof(struct inotify_event))
|
||||||
|
{
|
||||||
|
current = reinterpret_cast<inotify_event *>(buffer + i);
|
||||||
|
auto mask = current->mask;
|
||||||
|
|
||||||
|
int wd = current->wd;
|
||||||
|
auto itr = handlers.find(wd);
|
||||||
|
if (itr == end(handlers))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
FileNotifyType type;
|
||||||
|
if (mask & IN_CLOSE_WRITE)
|
||||||
|
type = FileNotifyType::FileChanged;
|
||||||
|
else if (mask & (IN_CREATE | IN_MOVED_TO))
|
||||||
|
type = FileNotifyType::FileCreated;
|
||||||
|
else if (mask & (IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM))
|
||||||
|
type = FileNotifyType::FileDeleted;
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (auto &func : itr->second.funcs)
|
||||||
|
{
|
||||||
|
if (func.func)
|
||||||
|
{
|
||||||
|
if (itr->second.directory)
|
||||||
|
{
|
||||||
|
auto notify_path = protocol + "://" + Path::join(func.path, current->name);
|
||||||
|
func.func({ std::move(notify_path), type, func.virtual_handle });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
func.func({ protocol + "://" + func.path, type, func.virtual_handle });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void OSFilesystem::uninstall_notification(FileNotifyHandle handle)
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
if (handle < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (notify_fd < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//LOGI("Uninstalling notification: %d\n", handle);
|
||||||
|
|
||||||
|
auto real = virtual_to_real.find(handle);
|
||||||
|
if (real == end(virtual_to_real))
|
||||||
|
{
|
||||||
|
LOGE("unknown virtual inotify handler.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto itr = handlers.find(static_cast<int>(real->second));
|
||||||
|
if (itr == end(handlers))
|
||||||
|
{
|
||||||
|
LOGE("unknown inotify handler.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto handler_instance = find_if(begin(itr->second.funcs), end(itr->second.funcs), [=](const VirtualHandler &v) {
|
||||||
|
return v.virtual_handle == handle;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (handler_instance == end(itr->second.funcs))
|
||||||
|
{
|
||||||
|
LOGE("unknown inotify handler path.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itr->second.funcs.erase(handler_instance);
|
||||||
|
|
||||||
|
if (itr->second.funcs.empty())
|
||||||
|
{
|
||||||
|
inotify_rm_watch(notify_fd, real->second);
|
||||||
|
handlers.erase(itr);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_to_real.erase(real);
|
||||||
|
#else
|
||||||
|
(void)handle;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FileNotifyHandle OSFilesystem::install_notification(const std::string &path,
|
||||||
|
std::function<void (const FileNotifyInfo &)> func)
|
||||||
|
{
|
||||||
|
#ifdef __linux__
|
||||||
|
//LOGI("Installing notification for: %s\n", path.c_str());
|
||||||
|
if (notify_fd < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
FileStat s = {};
|
||||||
|
if (!stat(path, s))
|
||||||
|
{
|
||||||
|
LOGE("inotify: path doesn't exist.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto resolved_path = Path::join(base, path);
|
||||||
|
int wd = inotify_add_watch(notify_fd, resolved_path.c_str(),
|
||||||
|
IN_MOVE | IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_DELETE_SELF);
|
||||||
|
|
||||||
|
if (wd < 0)
|
||||||
|
{
|
||||||
|
LOGE("Failed to create watch handle.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could have different paths which look different but resolve to the same wd, so handle that.
|
||||||
|
auto itr = handlers.find(wd);
|
||||||
|
if (itr == end(handlers))
|
||||||
|
handlers[wd] = { {{ path, std::move(func), ++virtual_handle }}, s.type == PathType::Directory };
|
||||||
|
else
|
||||||
|
itr->second.funcs.push_back({ path, std::move(func), ++virtual_handle });
|
||||||
|
|
||||||
|
//LOGI(" Got handle: %d\n", virtual_handle);
|
||||||
|
|
||||||
|
virtual_to_real[virtual_handle] = wd;
|
||||||
|
return static_cast<FileNotifyHandle>(virtual_handle);
|
||||||
|
#else
|
||||||
|
(void)path;
|
||||||
|
(void)func;
|
||||||
|
return -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OSFilesystem::remove(const std::string &path)
|
||||||
|
{
|
||||||
|
auto resolved_path = Path::join(base, path);
|
||||||
|
return unlink(resolved_path.c_str()) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OSFilesystem::move_yield(const std::string &dst, const std::string &src)
|
||||||
|
{
|
||||||
|
auto resolved_dst = Path::join(base, dst);
|
||||||
|
auto resolved_src = Path::join(base, src);
|
||||||
|
#if !defined(__linux__) || (defined(ANDROID) && (__ANDROID_API__ < __ANDROID_API_R__))
|
||||||
|
// Workaround since Android does not have renameat2 until API level 30.
|
||||||
|
// If we can exclusive create a new file, we can rename with replace somewhat safely.
|
||||||
|
int fd = ::open(resolved_dst.c_str(), O_EXCL | O_RDWR | O_CREAT, 0600);
|
||||||
|
if (fd >= 0)
|
||||||
|
{
|
||||||
|
::close(fd);
|
||||||
|
return rename(resolved_src.c_str(), resolved_dst.c_str()) == 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
return renameat2(AT_FDCWD, resolved_src.c_str(), AT_FDCWD, resolved_dst.c_str(), RENAME_NOREPLACE) == 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OSFilesystem::move_replace(const std::string &dst, const std::string &src)
|
||||||
|
{
|
||||||
|
auto resolved_dst = Path::join(base, dst);
|
||||||
|
auto resolved_src = Path::join(base, src);
|
||||||
|
return rename(resolved_src.c_str(), resolved_dst.c_str()) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListEntry> OSFilesystem::list(const std::string &path)
|
||||||
|
{
|
||||||
|
auto directory = Path::join(base, path);
|
||||||
|
DIR *dir = opendir(directory.c_str());
|
||||||
|
if (!dir)
|
||||||
|
{
|
||||||
|
LOGE("Failed to open directory %s\n", path.c_str());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListEntry> entries;
|
||||||
|
struct dirent *entry;
|
||||||
|
while ((entry = readdir(dir)))
|
||||||
|
{
|
||||||
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto joined_path = Path::join(path, entry->d_name);
|
||||||
|
|
||||||
|
PathType type;
|
||||||
|
if (entry->d_type == DT_DIR)
|
||||||
|
type = PathType::Directory;
|
||||||
|
else if (entry->d_type == DT_REG)
|
||||||
|
type = PathType::File;
|
||||||
|
else if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK)
|
||||||
|
type = PathType::Special;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FileStat s;
|
||||||
|
if (!stat(joined_path, s))
|
||||||
|
{
|
||||||
|
LOGE("Failed to stat file: %s\n", joined_path.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
type = s.type;
|
||||||
|
}
|
||||||
|
entries.push_back({ std::move(joined_path), type });
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OSFilesystem::stat(const std::string &path, FileStat &stat)
|
||||||
|
{
|
||||||
|
auto resolved_path = Path::join(base, path);
|
||||||
|
struct STAT64 buf = {};
|
||||||
|
if (::STAT64(resolved_path.c_str(), &buf) < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (S_ISREG(buf.st_mode))
|
||||||
|
stat.type = PathType::File;
|
||||||
|
else if (S_ISDIR(buf.st_mode))
|
||||||
|
stat.type = PathType::Directory;
|
||||||
|
else
|
||||||
|
stat.type = PathType::Special;
|
||||||
|
|
||||||
|
stat.size = uint64_t(buf.st_size);
|
||||||
|
#ifdef __linux__
|
||||||
|
stat.last_modified = buf.st_mtim.tv_sec * 1000000000ull + buf.st_mtim.tv_nsec;
|
||||||
|
#else
|
||||||
|
stat.last_modified = buf.st_mtimespec.tv_sec * 1000000000ull + buf.st_mtimespec.tv_nsec;
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
/* 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 "../filesystem.hpp"
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class MMapFile final : public File
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static FileHandle open(const std::string &path, FileMode mode);
|
||||||
|
~MMapFile() override;
|
||||||
|
FileMappingHandle map_subset(uint64_t offset, size_t size) override;
|
||||||
|
FileMappingHandle map_write(size_t map_size) override;
|
||||||
|
void unmap(void *mapped, size_t size) override;
|
||||||
|
uint64_t get_size() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool init(const std::string &path, FileMode mode);
|
||||||
|
bool query_stat();
|
||||||
|
int fd = -1;
|
||||||
|
size_t size = 0;
|
||||||
|
bool has_write_map = false;
|
||||||
|
std::string rename_from_on_close;
|
||||||
|
std::string rename_to_on_close;
|
||||||
|
|
||||||
|
void report_error();
|
||||||
|
};
|
||||||
|
|
||||||
|
class OSFilesystem : public FilesystemBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OSFilesystem(const std::string &base);
|
||||||
|
~OSFilesystem();
|
||||||
|
std::vector<ListEntry> list(const std::string &path) override;
|
||||||
|
FileHandle open(const std::string &path, FileMode mode) override;
|
||||||
|
bool stat(const std::string &path, FileStat &stat) override;
|
||||||
|
FileNotifyHandle install_notification(const std::string &path, std::function<void (const FileNotifyInfo &)> func) override;
|
||||||
|
void uninstall_notification(FileNotifyHandle handle) override;
|
||||||
|
void poll_notifications() override;
|
||||||
|
int get_notification_fd() const override;
|
||||||
|
std::string get_filesystem_path(const std::string &path) override;
|
||||||
|
|
||||||
|
bool remove(const std::string &path) override;
|
||||||
|
// Must implement atomic renaming semantics.
|
||||||
|
// Atomically replaces dst.
|
||||||
|
bool move_replace(const std::string &dst, const std::string &src) override;
|
||||||
|
// If dst exists, nothing happens (and false is returned).
|
||||||
|
bool move_yield(const std::string &dst, const std::string &src) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string base;
|
||||||
|
|
||||||
|
struct VirtualHandler
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
std::function<void (const FileNotifyInfo &)> func;
|
||||||
|
FileNotifyHandle virtual_handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Handler
|
||||||
|
{
|
||||||
|
std::vector<VirtualHandler> funcs;
|
||||||
|
bool directory;
|
||||||
|
};
|
||||||
|
std::unordered_map<FileNotifyHandle, Handler> handlers;
|
||||||
|
std::unordered_map<FileNotifyHandle, FileNotifyHandle> virtual_to_real;
|
||||||
|
int notify_fd;
|
||||||
|
FileNotifyHandle virtual_handle = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,895 @@
|
|||||||
|
/* 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 "fs-netfs.hpp"
|
||||||
|
#include "path_utils.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include <assert.h>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
#define HOST_IP "localhost"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
struct FSNotifyCommand : LooperHandler
|
||||||
|
{
|
||||||
|
FSNotifyCommand(const string &protocol, unique_ptr<Socket> socket_)
|
||||||
|
: LooperHandler(move(socket_)), expected(false)
|
||||||
|
{
|
||||||
|
reply_queue.emplace();
|
||||||
|
auto &reply = reply_queue.back();
|
||||||
|
reply.builder.add_u32(NETFS_NOTIFICATION);
|
||||||
|
reply.builder.add_u32(NETFS_BEGIN_CHUNK_REQUEST);
|
||||||
|
reply.builder.add_string(protocol);
|
||||||
|
reply.writer.start(reply.builder.get_buffer());
|
||||||
|
|
||||||
|
result_reply.begin(4 * sizeof(uint32_t));
|
||||||
|
command_reader.start(result_reply.get_buffer());
|
||||||
|
|
||||||
|
state = NotificationLoop;
|
||||||
|
}
|
||||||
|
|
||||||
|
void expected_destruction()
|
||||||
|
{
|
||||||
|
expected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~FSNotifyCommand()
|
||||||
|
{
|
||||||
|
if (!expected)
|
||||||
|
terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_notify_cb(function<void (const FileNotifyInfo &)> func)
|
||||||
|
{
|
||||||
|
notify_cb = move(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
void push_register_notification(const string &path, promise<FileNotifyHandle> result)
|
||||||
|
{
|
||||||
|
if (reply_queue.empty() && socket->get_parent_looper())
|
||||||
|
socket->get_parent_looper()->modify_handler(EVENT_IN | EVENT_OUT, *this);
|
||||||
|
|
||||||
|
reply_queue.emplace();
|
||||||
|
auto &reply = reply_queue.back();
|
||||||
|
reply.builder.add_u32(NETFS_REGISTER_NOTIFICATION);
|
||||||
|
reply.builder.add_string(path);
|
||||||
|
reply.writer.start(reply.builder.get_buffer());
|
||||||
|
|
||||||
|
replies.push(move(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
void push_unregister_notification(FileNotifyHandle handler, promise<FileNotifyHandle> result)
|
||||||
|
{
|
||||||
|
if (reply_queue.empty() && socket->get_parent_looper())
|
||||||
|
socket->get_parent_looper()->modify_handler(EVENT_IN | EVENT_OUT, *this);
|
||||||
|
|
||||||
|
reply_queue.emplace();
|
||||||
|
auto &reply = reply_queue.back();
|
||||||
|
reply.builder.add_u32(NETFS_UNREGISTER_NOTIFICATION);
|
||||||
|
reply.builder.add_u64(8);
|
||||||
|
reply.builder.add_u64(uint64_t(handler));
|
||||||
|
reply.writer.start(reply.builder.get_buffer());
|
||||||
|
replies.push(move(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
void modify_looper(Looper &looper)
|
||||||
|
{
|
||||||
|
uint32_t mask = reply_queue.empty() ? EVENT_IN : (EVENT_IN | EVENT_OUT);
|
||||||
|
looper.modify_handler(mask, *this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_reply_data(Looper &looper)
|
||||||
|
{
|
||||||
|
auto ret = command_reader.process(*socket);
|
||||||
|
if (command_reader.complete())
|
||||||
|
{
|
||||||
|
if (last_cmd == NETFS_BEGIN_CHUNK_NOTIFICATION)
|
||||||
|
{
|
||||||
|
FileNotifyInfo info;
|
||||||
|
info.path = result_reply.read_string();
|
||||||
|
info.handle = FileNotifyHandle(result_reply.read_u64());
|
||||||
|
auto type = result_reply.read_u32();
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case NETFS_FILE_CHANGED:
|
||||||
|
info.type = FileNotifyType::FileChanged;
|
||||||
|
break;
|
||||||
|
case NETFS_FILE_DELETED:
|
||||||
|
info.type = FileNotifyType::FileDeleted;
|
||||||
|
break;
|
||||||
|
case NETFS_FILE_CREATED:
|
||||||
|
info.type = FileNotifyType::FileCreated;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_cb(info);
|
||||||
|
result_reply.begin(4 * sizeof(uint32_t));
|
||||||
|
command_reader.start(result_reply.get_buffer());
|
||||||
|
modify_looper(looper);
|
||||||
|
state = NotificationLoop;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (last_cmd == NETFS_BEGIN_CHUNK_REPLY)
|
||||||
|
{
|
||||||
|
auto handle = int(result_reply.read_u64());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
replies.front().set_value(handle);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!replies.empty());
|
||||||
|
replies.pop();
|
||||||
|
|
||||||
|
result_reply.begin(4 * sizeof(uint32_t));
|
||||||
|
command_reader.start(result_reply.get_buffer());
|
||||||
|
modify_looper(looper);
|
||||||
|
state = NotificationLoop;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool notification_loop(Looper &looper, EventFlags flags)
|
||||||
|
{
|
||||||
|
if (flags & EVENT_OUT)
|
||||||
|
{
|
||||||
|
if (reply_queue.empty())
|
||||||
|
{
|
||||||
|
looper.modify_handler(EVENT_IN, *this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ret = reply_queue.front().writer.process(*socket);
|
||||||
|
if (reply_queue.front().writer.complete())
|
||||||
|
reply_queue.pop();
|
||||||
|
|
||||||
|
if (reply_queue.empty())
|
||||||
|
{
|
||||||
|
looper.modify_handler(EVENT_IN, *this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & EVENT_IN)
|
||||||
|
{
|
||||||
|
auto ret = command_reader.process(*socket);
|
||||||
|
if (command_reader.complete())
|
||||||
|
{
|
||||||
|
auto cmd = result_reply.read_u32();
|
||||||
|
if (cmd == NETFS_BEGIN_CHUNK_NOTIFICATION || cmd == NETFS_BEGIN_CHUNK_REPLY)
|
||||||
|
{
|
||||||
|
if (result_reply.read_u32() != NETFS_ERROR_OK)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
last_cmd = cmd;
|
||||||
|
auto size = result_reply.read_u64();
|
||||||
|
|
||||||
|
if (size)
|
||||||
|
{
|
||||||
|
// Either receive notification or acknowledgement.
|
||||||
|
result_reply.begin(size);
|
||||||
|
command_reader.start(result_reply.get_buffer());
|
||||||
|
state = ReadReplyData;
|
||||||
|
looper.modify_handler(EVENT_IN, *this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Acknowledge unregister notification.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
replies.front().set_value(0);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!replies.empty());
|
||||||
|
replies.pop();
|
||||||
|
|
||||||
|
result_reply.begin(4 * sizeof(uint32_t));
|
||||||
|
command_reader.start(result_reply.get_buffer());
|
||||||
|
modify_looper(looper);
|
||||||
|
state = NotificationLoop;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handle(Looper &looper, EventFlags flags) override
|
||||||
|
{
|
||||||
|
if (state == ReadReplyData)
|
||||||
|
return read_reply_data(looper);
|
||||||
|
else if (state == NotificationLoop)
|
||||||
|
return notification_loop(looper, flags);
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State
|
||||||
|
{
|
||||||
|
ReadReplyData,
|
||||||
|
NotificationLoop
|
||||||
|
};
|
||||||
|
|
||||||
|
State state = NotificationLoop;
|
||||||
|
SocketReader command_reader;
|
||||||
|
ReplyBuilder result_reply;
|
||||||
|
uint32_t last_cmd = 0;
|
||||||
|
|
||||||
|
struct NotificationReply
|
||||||
|
{
|
||||||
|
SocketWriter writer;
|
||||||
|
ReplyBuilder builder;
|
||||||
|
};
|
||||||
|
queue<NotificationReply> reply_queue;
|
||||||
|
queue<promise<FileNotifyHandle>> replies;
|
||||||
|
function<void (const FileNotifyInfo &info)> notify_cb;
|
||||||
|
atomic_bool expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FSReadCommand : LooperHandler
|
||||||
|
{
|
||||||
|
virtual ~FSReadCommand() = default;
|
||||||
|
|
||||||
|
FSReadCommand(const string &path, NetFSCommand command, unique_ptr<Socket> socket_)
|
||||||
|
: LooperHandler(move(socket_))
|
||||||
|
{
|
||||||
|
reply_builder.begin();
|
||||||
|
reply_builder.add_u32(command);
|
||||||
|
reply_builder.add_u32(NETFS_BEGIN_CHUNK_REQUEST);
|
||||||
|
reply_builder.add_string(path);
|
||||||
|
command_writer.start(reply_builder.get_buffer());
|
||||||
|
state = WriteCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_command(Looper &looper)
|
||||||
|
{
|
||||||
|
auto ret = command_writer.process(*socket);
|
||||||
|
if (command_writer.complete())
|
||||||
|
{
|
||||||
|
state = ReadReplySize;
|
||||||
|
reply_builder.begin(4 * sizeof(uint32_t));
|
||||||
|
command_reader.start(reply_builder.get_buffer());
|
||||||
|
looper.modify_handler(EVENT_IN, *this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_reply_size(Looper &)
|
||||||
|
{
|
||||||
|
auto ret = command_reader.process(*socket);
|
||||||
|
if (command_reader.complete())
|
||||||
|
{
|
||||||
|
if (reply_builder.read_u32() != NETFS_BEGIN_CHUNK_REPLY)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (reply_builder.read_u32() != NETFS_ERROR_OK)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint64_t reply_size = reply_builder.read_u64();
|
||||||
|
if (reply_size == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
reply_builder.begin(reply_size);
|
||||||
|
command_reader.start(reply_builder.get_buffer());
|
||||||
|
state = ReadReply;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_reply(Looper &)
|
||||||
|
{
|
||||||
|
auto ret = command_reader.process(*socket);
|
||||||
|
if (command_reader.complete())
|
||||||
|
{
|
||||||
|
parse_reply();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handle(Looper &looper, EventFlags) override
|
||||||
|
{
|
||||||
|
if (state == WriteCommand)
|
||||||
|
return write_command(looper);
|
||||||
|
else if (state == ReadReplySize)
|
||||||
|
return read_reply_size(looper);
|
||||||
|
else if (state == ReadReply)
|
||||||
|
return read_reply(looper);
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State
|
||||||
|
{
|
||||||
|
WriteCommand,
|
||||||
|
ReadReplySize,
|
||||||
|
ReadReply
|
||||||
|
};
|
||||||
|
State state = WriteCommand;
|
||||||
|
SocketReader command_reader;
|
||||||
|
SocketWriter command_writer;
|
||||||
|
ReplyBuilder reply_builder;
|
||||||
|
|
||||||
|
virtual void parse_reply() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FSReader : FSReadCommand
|
||||||
|
{
|
||||||
|
FSReader(const string &path, unique_ptr<Socket> socket_)
|
||||||
|
: FSReadCommand(path, NETFS_READ_FILE, move(socket_))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~FSReader()
|
||||||
|
{
|
||||||
|
if (!got_reply)
|
||||||
|
result.set_exception(make_exception_ptr(runtime_error("file read")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse_reply() override
|
||||||
|
{
|
||||||
|
got_reply = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.set_value(reply_builder.consume_buffer());
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promise<vector<uint8_t>> result;
|
||||||
|
bool got_reply = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FSList : FSReadCommand
|
||||||
|
{
|
||||||
|
FSList(const string &path, unique_ptr<Socket> socket_)
|
||||||
|
: FSReadCommand(path, NETFS_LIST, move(socket_))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~FSList()
|
||||||
|
{
|
||||||
|
if (!got_reply)
|
||||||
|
result.set_exception(make_exception_ptr(runtime_error("List failed")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse_reply() override
|
||||||
|
{
|
||||||
|
uint32_t entries = reply_builder.read_u32();
|
||||||
|
vector<ListEntry> list;
|
||||||
|
for (uint32_t i = 0; i < entries; i++)
|
||||||
|
{
|
||||||
|
auto path = reply_builder.read_string();
|
||||||
|
auto type = reply_builder.read_u32();
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case NETFS_FILE_TYPE_PLAIN:
|
||||||
|
list.push_back({ move(path), PathType::File });
|
||||||
|
break;
|
||||||
|
case NETFS_FILE_TYPE_DIRECTORY:
|
||||||
|
list.push_back({ move(path), PathType::Directory });
|
||||||
|
break;
|
||||||
|
case NETFS_FILE_TYPE_SPECIAL:
|
||||||
|
list.push_back({ move(path), PathType::Special });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
got_reply = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.set_value(move(list));
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promise<vector<ListEntry>> result;
|
||||||
|
bool got_reply = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FSStat : FSReadCommand
|
||||||
|
{
|
||||||
|
FSStat(const string &path, unique_ptr<Socket> socket_)
|
||||||
|
: FSReadCommand(path, NETFS_STAT, move(socket_))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~FSStat()
|
||||||
|
{
|
||||||
|
// Throw exception instead in calling thread.
|
||||||
|
if (!got_reply)
|
||||||
|
result.set_exception(make_exception_ptr(runtime_error("Failed stat")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse_reply() override
|
||||||
|
{
|
||||||
|
uint64_t size = reply_builder.read_u64();
|
||||||
|
uint32_t type = reply_builder.read_u32();
|
||||||
|
uint64_t last_modified = reply_builder.read_u64();
|
||||||
|
FileStat s;
|
||||||
|
s.size = size;
|
||||||
|
s.last_modified = last_modified;
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case NETFS_FILE_TYPE_PLAIN:
|
||||||
|
s.type = PathType::File;
|
||||||
|
break;
|
||||||
|
case NETFS_FILE_TYPE_DIRECTORY:
|
||||||
|
s.type = PathType::Directory;
|
||||||
|
break;
|
||||||
|
case NETFS_FILE_TYPE_SPECIAL:
|
||||||
|
s.type = PathType::Special;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
got_reply = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.set_value(s);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::promise<FileStat> result;
|
||||||
|
bool got_reply = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FSWriteCommand : LooperHandler
|
||||||
|
{
|
||||||
|
FSWriteCommand(const string &path, const vector<uint8_t> &buffer, unique_ptr<Socket> socket_)
|
||||||
|
: LooperHandler(move(socket_))
|
||||||
|
{
|
||||||
|
target_size = buffer.size();
|
||||||
|
|
||||||
|
reply_builder.begin();
|
||||||
|
result_reply.begin(4 * sizeof(uint32_t));
|
||||||
|
|
||||||
|
reply_builder.add_u32(NETFS_WRITE_FILE);
|
||||||
|
reply_builder.add_u32(NETFS_BEGIN_CHUNK_REQUEST);
|
||||||
|
reply_builder.add_string(path);
|
||||||
|
reply_builder.add_u32(NETFS_BEGIN_CHUNK_REQUEST);
|
||||||
|
reply_builder.add_u64(buffer.size());
|
||||||
|
reply_builder.add_buffer(buffer);
|
||||||
|
command_writer.start(reply_builder.get_buffer());
|
||||||
|
command_reader.start(result_reply.get_buffer());
|
||||||
|
state = WriteCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_command(Looper &looper, EventFlags flags)
|
||||||
|
{
|
||||||
|
if (flags & EVENT_IN)
|
||||||
|
{
|
||||||
|
auto ret = command_reader.process(*socket);
|
||||||
|
// Received message before we completed the write, must be an error.
|
||||||
|
if (command_reader.complete())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
else if (flags & EVENT_OUT)
|
||||||
|
{
|
||||||
|
auto ret = command_writer.process(*socket);
|
||||||
|
if (command_writer.complete())
|
||||||
|
{
|
||||||
|
// Done writing, wait for reply.
|
||||||
|
looper.modify_handler(EVENT_IN, *this);
|
||||||
|
state = ReadReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~FSWriteCommand()
|
||||||
|
{
|
||||||
|
if (!got_reply)
|
||||||
|
result.set_exception(make_exception_ptr(runtime_error("Failed write")));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_reply(Looper &)
|
||||||
|
{
|
||||||
|
auto ret = command_reader.process(*socket);
|
||||||
|
if (command_reader.complete())
|
||||||
|
{
|
||||||
|
if (result_reply.read_u32() != NETFS_BEGIN_CHUNK_REPLY)
|
||||||
|
return false;
|
||||||
|
if (result_reply.read_u32() != NETFS_ERROR_OK)
|
||||||
|
return false;
|
||||||
|
if (result_reply.read_u64() != target_size)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
got_reply = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.set_value(NETFS_ERROR_OK);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ret > 0) || (ret == Socket::ErrorWouldBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handle(Looper &looper, EventFlags flags) override
|
||||||
|
{
|
||||||
|
if (state == WriteCommand)
|
||||||
|
return write_command(looper, flags);
|
||||||
|
else if (state == ReadReply)
|
||||||
|
return read_reply(looper);
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State
|
||||||
|
{
|
||||||
|
WriteCommand,
|
||||||
|
ReadReply
|
||||||
|
};
|
||||||
|
|
||||||
|
State state = WriteCommand;
|
||||||
|
SocketReader command_reader;
|
||||||
|
SocketWriter command_writer;
|
||||||
|
ReplyBuilder reply_builder;
|
||||||
|
ReplyBuilder result_reply;
|
||||||
|
size_t target_size = 0;
|
||||||
|
|
||||||
|
promise<NetFSError> result;
|
||||||
|
bool got_reply = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkFilesystem::NetworkFilesystem()
|
||||||
|
{
|
||||||
|
looper_thread = thread(&NetworkFilesystem::looper_entry, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkFilesystem::looper_entry()
|
||||||
|
{
|
||||||
|
while (looper.wait_idle(-1) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkFilesystem::setup_notification()
|
||||||
|
{
|
||||||
|
auto socket = Socket::connect(HOST_IP, 7070);
|
||||||
|
if (!socket)
|
||||||
|
return;
|
||||||
|
notify = new FSNotifyCommand(protocol, move(socket));
|
||||||
|
notify->set_notify_cb([this](const FileNotifyInfo &info) {
|
||||||
|
signal_notification(info);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move capture would be nice ...
|
||||||
|
looper.run_in_looper([this]() {
|
||||||
|
looper.register_handler(EVENT_OUT, unique_ptr<FSNotifyCommand>(notify));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkFilesystem::uninstall_notification(FileNotifyHandle handle)
|
||||||
|
{
|
||||||
|
if (!notify)
|
||||||
|
setup_notification();
|
||||||
|
if (!notify)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto itr = handlers.find(handle);
|
||||||
|
if (itr == end(handlers))
|
||||||
|
return;
|
||||||
|
handlers.erase(itr);
|
||||||
|
|
||||||
|
auto *value = new promise<FileNotifyHandle>;
|
||||||
|
auto result = value->get_future();
|
||||||
|
looper.run_in_looper([this, value, handle]() {
|
||||||
|
notify->push_unregister_notification(handle, move(*value));
|
||||||
|
delete value;
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.wait();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkFilesystem::signal_notification(const FileNotifyInfo &info)
|
||||||
|
{
|
||||||
|
lock_guard<mutex> holder{lock};
|
||||||
|
pending.push_back(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkFilesystem::poll_notifications()
|
||||||
|
{
|
||||||
|
vector<FileNotifyInfo> tmp_pending;
|
||||||
|
{
|
||||||
|
lock_guard<mutex> holder{lock};
|
||||||
|
swap(tmp_pending, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto ¬ification : tmp_pending)
|
||||||
|
{
|
||||||
|
auto &func = handlers[notification.handle];
|
||||||
|
if (func)
|
||||||
|
func(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileNotifyHandle NetworkFilesystem::install_notification(const std::string &path,
|
||||||
|
std::function<void(const FileNotifyInfo &)> func)
|
||||||
|
{
|
||||||
|
if (!notify)
|
||||||
|
setup_notification();
|
||||||
|
if (!notify)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
auto *value = new promise<FileNotifyHandle>;
|
||||||
|
auto result = value->get_future();
|
||||||
|
|
||||||
|
looper.run_in_looper([this, value, path]() {
|
||||||
|
notify->push_register_notification(path, move(*value));
|
||||||
|
delete value;
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto handle = result.get();
|
||||||
|
handlers[handle] = move(func);
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<ListEntry> NetworkFilesystem::list(const std::string &path)
|
||||||
|
{
|
||||||
|
auto joined = protocol + "://" + path;
|
||||||
|
auto socket = Socket::connect(HOST_IP, 7070);
|
||||||
|
if (!socket)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
unique_ptr<FSList> handler(new FSList(joined, move(socket)));
|
||||||
|
auto fut = handler->result.get_future();
|
||||||
|
|
||||||
|
looper.run_in_looper([&]() {
|
||||||
|
looper.register_handler(EVENT_OUT, move(handler));
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return fut.get();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkFile::~NetworkFile()
|
||||||
|
{
|
||||||
|
unmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkFile *NetworkFile::open(Granite::Looper &looper, const std::string &path, Granite::FileMode mode)
|
||||||
|
{
|
||||||
|
auto *file = new NetworkFile;
|
||||||
|
if (!file->init(looper, path, mode))
|
||||||
|
{
|
||||||
|
delete file;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkFile::init(Looper &looper_, const std::string &path_, FileMode mode_)
|
||||||
|
{
|
||||||
|
path = path_;
|
||||||
|
mode = mode_;
|
||||||
|
looper = &looper_;
|
||||||
|
|
||||||
|
if (mode == FileMode::ReadWrite)
|
||||||
|
{
|
||||||
|
LOGE("Unsupported file mode.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == FileMode::ReadOnly)
|
||||||
|
{
|
||||||
|
if (!reopen())
|
||||||
|
{
|
||||||
|
LOGE("Failed to connect to server.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkFile::unmap()
|
||||||
|
{
|
||||||
|
if (mode == FileMode::WriteOnly && has_buffer && need_flush)
|
||||||
|
{
|
||||||
|
need_flush = false;
|
||||||
|
auto socket = Socket::connect(HOST_IP, 7070);
|
||||||
|
if (!socket)
|
||||||
|
throw runtime_error("Failed to connect to server.");
|
||||||
|
|
||||||
|
auto handler = unique_ptr<FSWriteCommand>(new FSWriteCommand(path, buffer, move(socket)));
|
||||||
|
auto reply = handler->result.get_future();
|
||||||
|
looper->run_in_looper([&handler, this]() {
|
||||||
|
looper->register_handler(EVENT_OUT | EVENT_IN, move(handler));
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
NetFSError error = reply.get();
|
||||||
|
if (error != NETFS_ERROR_OK)
|
||||||
|
LOGE("Failed to write file: %s\n", path.c_str());
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
LOGE("Failed to write file: %s\n", path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkFile::reopen()
|
||||||
|
{
|
||||||
|
if (mode == FileMode::ReadOnly)
|
||||||
|
{
|
||||||
|
has_buffer = false;
|
||||||
|
auto socket = Socket::connect(HOST_IP, 7070);
|
||||||
|
if (!socket)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto *handler = new FSReader(path, move(socket));
|
||||||
|
future = handler->result.get_future();
|
||||||
|
|
||||||
|
// Capture-by-move would be nice here.
|
||||||
|
looper->run_in_looper([handler, this]() {
|
||||||
|
looper->register_handler(EVENT_OUT, unique_ptr<FSReader>(handler));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *NetworkFile::map_write(size_t size)
|
||||||
|
{
|
||||||
|
has_buffer = true;
|
||||||
|
need_flush = true;
|
||||||
|
buffer.resize(size);
|
||||||
|
return buffer.empty() ? nullptr : buffer.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
void *NetworkFile::map()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!has_buffer)
|
||||||
|
{
|
||||||
|
buffer = future.get();
|
||||||
|
has_buffer = true;
|
||||||
|
}
|
||||||
|
return buffer.empty() ? nullptr : buffer.data();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t NetworkFile::get_size()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!has_buffer)
|
||||||
|
{
|
||||||
|
buffer = future.get();
|
||||||
|
has_buffer = true;
|
||||||
|
}
|
||||||
|
return buffer.size();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unique_ptr<File> NetworkFilesystem::open(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
auto joined = protocol + "://" + path;
|
||||||
|
return unique_ptr<File>(NetworkFile::open(looper, move(joined), mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkFilesystem::stat(const std::string &path, FileStat &stat)
|
||||||
|
{
|
||||||
|
auto joined = protocol + "://" + path;
|
||||||
|
auto socket = Socket::connect(HOST_IP, 7070);
|
||||||
|
if (!socket)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
unique_ptr<FSStat> handler(new FSStat(joined, move(socket)));
|
||||||
|
auto fut = handler->result.get_future();
|
||||||
|
|
||||||
|
looper.run_in_looper([&]() {
|
||||||
|
looper.register_handler(EVENT_OUT, move(handler));
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stat = fut.get();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkFilesystem::~NetworkFilesystem()
|
||||||
|
{
|
||||||
|
if (notify)
|
||||||
|
notify->expected_destruction();
|
||||||
|
|
||||||
|
looper.kill();
|
||||||
|
if (looper_thread.joinable())
|
||||||
|
looper_thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/* 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 "fs-netfs.hpp"
|
||||||
|
#include "network.hpp"
|
||||||
|
#include "../filesystem.hpp"
|
||||||
|
#include "netfs.hpp"
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <future>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
struct FSReader;
|
||||||
|
class NetworkFile : public File
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static NetworkFile *open(Looper &looper, const std::string &path, FileMode mode);
|
||||||
|
~NetworkFile();
|
||||||
|
void *map() override;
|
||||||
|
void *map_write(size_t size) override;
|
||||||
|
void unmap() override;
|
||||||
|
size_t get_size() override;
|
||||||
|
bool reopen() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
NetworkFile() = default;
|
||||||
|
bool init(Looper &looper, const std::string &path, FileMode mode);
|
||||||
|
std::string path;
|
||||||
|
FileMode mode;
|
||||||
|
Looper *looper = nullptr;
|
||||||
|
std::future<std::vector<uint8_t>> future;
|
||||||
|
std::vector<uint8_t> buffer;
|
||||||
|
bool has_buffer = false;
|
||||||
|
bool need_flush = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FSNotifyCommand;
|
||||||
|
class NetworkFilesystem : public FilesystemBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NetworkFilesystem();
|
||||||
|
~NetworkFilesystem();
|
||||||
|
std::vector<ListEntry> list(const std::string &path) override;
|
||||||
|
std::unique_ptr<File> open(const std::string &path, FileMode mode) override;
|
||||||
|
bool stat(const std::string &path, FileStat &stat) override;
|
||||||
|
|
||||||
|
FileNotifyHandle install_notification(const std::string &path, std::function<void (const FileNotifyInfo &)> func) override;
|
||||||
|
|
||||||
|
void uninstall_notification(FileNotifyHandle handle) override;
|
||||||
|
|
||||||
|
void poll_notifications() override;
|
||||||
|
|
||||||
|
int get_notification_fd() const override
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::thread looper_thread;
|
||||||
|
Looper looper;
|
||||||
|
void looper_entry();
|
||||||
|
FSNotifyCommand *notify = nullptr;
|
||||||
|
|
||||||
|
std::unordered_map<FileNotifyHandle, std::function<void (const FileNotifyInfo &)>> handlers;
|
||||||
|
std::mutex lock;
|
||||||
|
std::vector<FileNotifyInfo> pending;
|
||||||
|
|
||||||
|
void setup_notification();
|
||||||
|
void signal_notification(const FileNotifyInfo &info);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/* 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 "path_utils.hpp"
|
||||||
|
#include "filesystem.hpp"
|
||||||
|
#include "intrusive.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
class VolatileSource : public Util::IntrusivePtrEnabled<VolatileSource<T>>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VolatileSource(Filesystem *fs_, const std::string &path_)
|
||||||
|
: fs(fs_), path(Path::enforce_protocol(path_))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
VolatileSource() = default;
|
||||||
|
|
||||||
|
~VolatileSource()
|
||||||
|
{
|
||||||
|
deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Filesystem *fs = nullptr;
|
||||||
|
std::string path;
|
||||||
|
void deinit()
|
||||||
|
{
|
||||||
|
if (notify_backend && notify_handle >= 0)
|
||||||
|
notify_backend->uninstall_notification(notify_handle);
|
||||||
|
notify_backend = nullptr;
|
||||||
|
notify_handle = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool init()
|
||||||
|
{
|
||||||
|
if (path.empty() || !fs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto file = fs->open_readonly_mapping(path);
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
LOGE("Failed to open volatile file: %s\n", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *self = static_cast<T *>(this);
|
||||||
|
self->update(std::move(file));
|
||||||
|
|
||||||
|
auto paths = Path::protocol_split(path);
|
||||||
|
auto *proto = fs->get_backend(paths.first);
|
||||||
|
if (proto)
|
||||||
|
{
|
||||||
|
// Listen to directory so we can track file moves properly.
|
||||||
|
notify_handle = proto->install_notification(Path::basedir(paths.second), [&](const FileNotifyInfo &info) {
|
||||||
|
if (info.type == FileNotifyType::FileDeleted)
|
||||||
|
return;
|
||||||
|
if (info.path != path)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto f = fs->open_readonly_mapping(info.path);
|
||||||
|
if (!f)
|
||||||
|
return;
|
||||||
|
auto *s = static_cast<T *>(this);
|
||||||
|
s->update(std::move(f));
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
LOGE("Caught update exception: %s\n", e.what());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
FileNotifyHandle notify_handle = -1;
|
||||||
|
FilesystemBackend *notify_backend = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using VolatileHandle = Util::IntrusivePtr<VolatileSource<T>>;
|
||||||
|
}
|
||||||
+471
@@ -0,0 +1,471 @@
|
|||||||
|
/* 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 "os_filesystem.hpp"
|
||||||
|
#include "path_utils.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
static bool ensure_directory_inner(const std::string &path)
|
||||||
|
{
|
||||||
|
if (Path::is_root_path(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto wpath = Path::to_utf16(path);
|
||||||
|
|
||||||
|
struct __stat64 s;
|
||||||
|
if (::_wstat64(wpath.c_str(), &s) >= 0 && (s.st_mode & _S_IFDIR) != 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
auto basedir = Path::basedir(path);
|
||||||
|
if (!ensure_directory_inner(basedir))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!CreateDirectoryW(wpath.c_str(), nullptr))
|
||||||
|
return GetLastError() == ERROR_ALREADY_EXISTS;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ensure_directory(const std::string &path)
|
||||||
|
{
|
||||||
|
auto basedir = Path::basedir(path);
|
||||||
|
return ensure_directory_inner(basedir);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHandle MappedFile::open(const std::string &path, Granite::FileMode mode)
|
||||||
|
{
|
||||||
|
auto file = Util::make_handle<MappedFile>();
|
||||||
|
if (!file->init(path, mode))
|
||||||
|
file.reset();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic_uint32_t global_transaction_counter;
|
||||||
|
|
||||||
|
bool MappedFile::init(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
DWORD access = 0;
|
||||||
|
DWORD disposition = 0;
|
||||||
|
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case FileMode::ReadOnly:
|
||||||
|
access = GENERIC_READ;
|
||||||
|
disposition = OPEN_EXISTING;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileMode::ReadWrite:
|
||||||
|
if (!ensure_directory(path))
|
||||||
|
{
|
||||||
|
LOGE("MappedFile failed to create directory.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
access = GENERIC_READ | GENERIC_WRITE;
|
||||||
|
disposition = OPEN_ALWAYS;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileMode::WriteOnly:
|
||||||
|
case FileMode::WriteOnlyTransactional:
|
||||||
|
if (!ensure_directory(path))
|
||||||
|
{
|
||||||
|
LOGE("MappedFile failed to create directory.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
access = GENERIC_READ | GENERIC_WRITE;
|
||||||
|
disposition = CREATE_ALWAYS;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == FileMode::WriteOnlyTransactional)
|
||||||
|
{
|
||||||
|
// Use atomic file rename to ensure that a file is written atomically.
|
||||||
|
rename_to_on_close = path;
|
||||||
|
rename_from_on_close =
|
||||||
|
path + ".tmp." +
|
||||||
|
std::to_string(GetCurrentProcessId()) + "." +
|
||||||
|
std::to_string(global_transaction_counter.fetch_add(1, std::memory_order_relaxed));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto wpath = Path::to_utf16(rename_from_on_close.empty() ? path : rename_from_on_close);
|
||||||
|
|
||||||
|
file = CreateFileW(wpath.c_str(), access, FILE_SHARE_READ, nullptr, disposition,
|
||||||
|
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, INVALID_HANDLE_VALUE);
|
||||||
|
if (file == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
rename_to_on_close.clear();
|
||||||
|
rename_from_on_close.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode != FileMode::WriteOnly && mode != FileMode::WriteOnlyTransactional)
|
||||||
|
{
|
||||||
|
DWORD hi;
|
||||||
|
DWORD lo = GetFileSize(file, &hi);
|
||||||
|
size = (uint64_t(hi) << 32) | uint32_t(lo);
|
||||||
|
file_mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t MappedFile::get_size()
|
||||||
|
{
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PageSizeQuery
|
||||||
|
{
|
||||||
|
PageSizeQuery()
|
||||||
|
{
|
||||||
|
SYSTEM_INFO system_info = {};
|
||||||
|
GetSystemInfo(&system_info);
|
||||||
|
page_size = system_info.dwPageSize;
|
||||||
|
}
|
||||||
|
uint32_t page_size = 0;
|
||||||
|
};
|
||||||
|
static PageSizeQuery static_page_size_query;
|
||||||
|
|
||||||
|
FileMappingHandle MappedFile::map_subset(uint64_t offset, size_t range)
|
||||||
|
{
|
||||||
|
if (offset + range > size)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (!file_mapping)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
uint64_t begin_map = offset & ~uint64_t(static_page_size_query.page_size - 1);
|
||||||
|
|
||||||
|
DWORD hi = DWORD(begin_map >> 32);
|
||||||
|
DWORD lo = DWORD(begin_map & 0xffffffffu);
|
||||||
|
uint64_t end_mapping = offset + range;
|
||||||
|
size_t mapped_size = end_mapping - begin_map;
|
||||||
|
|
||||||
|
void *mapped = MapViewOfFile(file_mapping, FILE_MAP_READ, hi, lo, mapped_size);
|
||||||
|
if (!mapped)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return Util::make_handle<FileMapping>(
|
||||||
|
reference_from_this(), offset,
|
||||||
|
static_cast<uint8_t *>(mapped) + begin_map, mapped_size,
|
||||||
|
offset - begin_map, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMappingHandle MappedFile::map_write(size_t map_size)
|
||||||
|
{
|
||||||
|
size = map_size;
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
DWORD hi = DWORD(size >> 32);
|
||||||
|
DWORD lo = DWORD(size & 0xffffffffu);
|
||||||
|
#else
|
||||||
|
DWORD hi = 0;
|
||||||
|
DWORD lo = DWORD(size);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HANDLE file_view = CreateFileMappingW(file, nullptr, PAGE_READWRITE, hi, lo, nullptr);
|
||||||
|
if (!file_view)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
void *mapped = MapViewOfFile(file_view, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||||
|
CloseHandle(file_view);
|
||||||
|
|
||||||
|
if (!mapped)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return Util::make_handle<FileMapping>(reference_from_this(), 0,
|
||||||
|
static_cast<uint8_t *>(mapped), size,
|
||||||
|
0, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MappedFile::unmap(void *mapped, size_t)
|
||||||
|
{
|
||||||
|
if (mapped)
|
||||||
|
UnmapViewOfFile(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
MappedFile::~MappedFile()
|
||||||
|
{
|
||||||
|
if (file_mapping)
|
||||||
|
CloseHandle(file_mapping);
|
||||||
|
if (file != INVALID_HANDLE_VALUE)
|
||||||
|
CloseHandle(file);
|
||||||
|
|
||||||
|
if (!rename_from_on_close.empty() && !rename_to_on_close.empty())
|
||||||
|
{
|
||||||
|
auto to_w16 = Path::to_utf16(rename_to_on_close);
|
||||||
|
auto from_w16 = Path::to_utf16(rename_from_on_close);
|
||||||
|
DWORD code = S_OK;
|
||||||
|
|
||||||
|
if (!MoveFileW(from_w16.c_str(), to_w16.c_str()))
|
||||||
|
{
|
||||||
|
code = GetLastError();
|
||||||
|
if (code == ERROR_ALREADY_EXISTS && !ReplaceFileW(to_w16.c_str(), from_w16.c_str(), nullptr, 0, nullptr, nullptr))
|
||||||
|
code = GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FAILED(code))
|
||||||
|
LOGE("Failed to rename file %s -> %s (0x%lx).\n", rename_from_on_close.c_str(), rename_to_on_close.c_str(), code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OSFilesystem::OSFilesystem(const std::string &base_)
|
||||||
|
: base(base_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
OSFilesystem::~OSFilesystem()
|
||||||
|
{
|
||||||
|
for (auto &handler : handlers)
|
||||||
|
{
|
||||||
|
CancelIo(handler.second.handle);
|
||||||
|
CloseHandle(handler.second.handle);
|
||||||
|
CloseHandle(handler.second.event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OSFilesystem::get_filesystem_path(const std::string &path)
|
||||||
|
{
|
||||||
|
return Path::join(base, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHandle OSFilesystem::open(const std::string &path, FileMode mode)
|
||||||
|
{
|
||||||
|
return MappedFile::open(Path::join(base, path), mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OSFilesystem::poll_notifications()
|
||||||
|
{
|
||||||
|
for (auto &handler : handlers)
|
||||||
|
{
|
||||||
|
if (WaitForSingleObject(handler.second.event, 0) != WAIT_OBJECT_0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
DWORD bytes_returned;
|
||||||
|
if (!GetOverlappedResult(handler.second.handle, &handler.second.overlapped, &bytes_returned, TRUE))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
const FILE_NOTIFY_INFORMATION *info = nullptr;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
info = reinterpret_cast<const FILE_NOTIFY_INFORMATION *>(
|
||||||
|
reinterpret_cast<const uint8_t *>(handler.second.async_buffer) + offset);
|
||||||
|
|
||||||
|
FileNotifyInfo notify;
|
||||||
|
notify.handle = handler.first;
|
||||||
|
notify.path = Path::join(handler.second.path,
|
||||||
|
Path::to_utf8(info->FileName,
|
||||||
|
info->FileNameLength / sizeof(wchar_t)));
|
||||||
|
|
||||||
|
switch (info->Action)
|
||||||
|
{
|
||||||
|
case FILE_ACTION_ADDED:
|
||||||
|
case FILE_ACTION_RENAMED_NEW_NAME:
|
||||||
|
notify.type = FileNotifyType::FileCreated;
|
||||||
|
if (handler.second.func)
|
||||||
|
handler.second.func(notify);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FILE_ACTION_REMOVED:
|
||||||
|
case FILE_ACTION_RENAMED_OLD_NAME:
|
||||||
|
notify.type = FileNotifyType::FileDeleted;
|
||||||
|
if (handler.second.func)
|
||||||
|
handler.second.func(notify);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FILE_ACTION_MODIFIED:
|
||||||
|
notify.type = FileNotifyType::FileChanged;
|
||||||
|
if (handler.second.func)
|
||||||
|
handler.second.func(notify);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOGE("Invalid notify type.\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += info->NextEntryOffset;
|
||||||
|
} while (info->NextEntryOffset != 0);
|
||||||
|
|
||||||
|
kick_async(handler.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OSFilesystem::uninstall_notification(FileNotifyHandle id)
|
||||||
|
{
|
||||||
|
auto itr = handlers.find(id);
|
||||||
|
if (itr != end(handlers))
|
||||||
|
{
|
||||||
|
CancelIo(itr->second.handle);
|
||||||
|
CloseHandle(itr->second.handle);
|
||||||
|
CloseHandle(itr->second.event);
|
||||||
|
handlers.erase(itr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OSFilesystem::kick_async(Handler &handler)
|
||||||
|
{
|
||||||
|
handler.overlapped = {};
|
||||||
|
handler.overlapped.hEvent = handler.event;
|
||||||
|
|
||||||
|
auto ret = ReadDirectoryChangesW(handler.handle, handler.async_buffer, sizeof(handler.async_buffer), FALSE,
|
||||||
|
FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,
|
||||||
|
nullptr, &handler.overlapped, nullptr);
|
||||||
|
|
||||||
|
if (!ret && GetLastError() != ERROR_IO_PENDING)
|
||||||
|
{
|
||||||
|
LOGE("Failed to read directory changes async.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileNotifyHandle OSFilesystem::install_notification(const std::string &path, std::function<void(const FileNotifyInfo &)> func)
|
||||||
|
{
|
||||||
|
FileStat s = {};
|
||||||
|
if (!stat(path, s))
|
||||||
|
{
|
||||||
|
LOGE("Window inotify: path doesn't exist.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.type != PathType::Directory)
|
||||||
|
{
|
||||||
|
LOGE("Windows inotify: Implementation only supports directories.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto resolved_path = Path::to_utf16(Path::join(base, path));
|
||||||
|
HANDLE handle =
|
||||||
|
CreateFileW(resolved_path.c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
|
||||||
|
nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr);
|
||||||
|
if (handle == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
LOGE("Failed to open directory for watching.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE event = CreateEvent(nullptr, FALSE, FALSE, nullptr);
|
||||||
|
if (event == nullptr)
|
||||||
|
{
|
||||||
|
CloseHandle(handle);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_id++;
|
||||||
|
Handler handler;
|
||||||
|
handler.path = protocol + "://" + path;
|
||||||
|
handler.func = std::move(func);
|
||||||
|
handler.handle = handle;
|
||||||
|
handler.event = event;
|
||||||
|
auto &h = handlers[handle_id];
|
||||||
|
h = std::move(handler);
|
||||||
|
kick_async(h);
|
||||||
|
|
||||||
|
return handle_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OSFilesystem::remove(const std::string &path)
|
||||||
|
{
|
||||||
|
auto joined = Path::to_utf16(Path::join(base, path));
|
||||||
|
return bool(DeleteFileW(joined.c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OSFilesystem::move_yield(const std::string &dst, const std::string &src)
|
||||||
|
{
|
||||||
|
auto joined_dst = Path::to_utf16(Path::join(base, dst));
|
||||||
|
auto joined_src = Path::to_utf16(Path::join(base, src));
|
||||||
|
return bool(MoveFileW(joined_src.c_str(), joined_dst.c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OSFilesystem::move_replace(const std::string &dst, const std::string &src)
|
||||||
|
{
|
||||||
|
auto joined_dst = Path::to_utf16(Path::join(base, dst));
|
||||||
|
auto joined_src = Path::to_utf16(Path::join(base, src));
|
||||||
|
if (MoveFileW(joined_src.c_str(), joined_dst.c_str()))
|
||||||
|
return true;
|
||||||
|
if (GetLastError() != ERROR_ALREADY_EXISTS)
|
||||||
|
return false;
|
||||||
|
return bool(ReplaceFileW(joined_dst.c_str(), joined_src.c_str(), nullptr, 0, nullptr, nullptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListEntry> OSFilesystem::list(const std::string &path)
|
||||||
|
{
|
||||||
|
std::vector<ListEntry> entries;
|
||||||
|
WIN32_FIND_DATAW result;
|
||||||
|
auto joined = Path::to_utf16(Path::join(base, path));
|
||||||
|
joined += L"/*";
|
||||||
|
|
||||||
|
HANDLE handle = FindFirstFileW(joined.c_str(), &result);
|
||||||
|
if (handle == INVALID_HANDLE_VALUE)
|
||||||
|
return entries;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
ListEntry entry;
|
||||||
|
if (result.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||||
|
entry.type = PathType::Directory;
|
||||||
|
else
|
||||||
|
entry.type = PathType::File;
|
||||||
|
|
||||||
|
auto utf8_path = Path::to_utf8(result.cFileName);
|
||||||
|
if (utf8_path == "." || utf8_path == "..")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
entry.path = Path::join(path, utf8_path);
|
||||||
|
entries.push_back(std::move(entry));
|
||||||
|
} while (FindNextFileW(handle, &result));
|
||||||
|
|
||||||
|
FindClose(handle);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OSFilesystem::stat(const std::string &path, FileStat &stat)
|
||||||
|
{
|
||||||
|
auto joined = Path::join(base, path);
|
||||||
|
struct __stat64 buf;
|
||||||
|
if (_wstat64(Path::to_utf16(joined).c_str(), &buf) < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (buf.st_mode & _S_IFREG)
|
||||||
|
stat.type = PathType::File;
|
||||||
|
else if (buf.st_mode & _S_IFDIR)
|
||||||
|
stat.type = PathType::Directory;
|
||||||
|
else
|
||||||
|
stat.type = PathType::Special;
|
||||||
|
|
||||||
|
stat.size = uint64_t(buf.st_size);
|
||||||
|
stat.last_modified = buf.st_mtime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int OSFilesystem::get_notification_fd() const
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Granite
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
/* 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 "filesystem.hpp"
|
||||||
|
#include <unordered_map>
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class MappedFile final : public File
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static FileHandle open(const std::string &path, FileMode mode);
|
||||||
|
~MappedFile() override;
|
||||||
|
|
||||||
|
FileMappingHandle map_subset(uint64_t offset, size_t range) override;
|
||||||
|
FileMappingHandle map_write(size_t size) override;
|
||||||
|
void unmap(void *mapped, size_t range) override;
|
||||||
|
uint64_t get_size() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool init(const std::string &path, FileMode mode);
|
||||||
|
HANDLE file = INVALID_HANDLE_VALUE;
|
||||||
|
HANDLE file_mapping = nullptr;
|
||||||
|
uint64_t size = 0;
|
||||||
|
std::string rename_from_on_close;
|
||||||
|
std::string rename_to_on_close;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OSFilesystem : public FilesystemBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OSFilesystem(const std::string &base);
|
||||||
|
~OSFilesystem();
|
||||||
|
std::vector<ListEntry> list(const std::string &path) override;
|
||||||
|
FileHandle open(const std::string &path, FileMode mode) override;
|
||||||
|
bool stat(const std::string &path, FileStat &stat) override;
|
||||||
|
FileNotifyHandle install_notification(const std::string &path, std::function<void (const FileNotifyInfo &)> func) override;
|
||||||
|
void uninstall_notification(FileNotifyHandle handle) override;
|
||||||
|
void poll_notifications() override;
|
||||||
|
int get_notification_fd() const override;
|
||||||
|
std::string get_filesystem_path(const std::string &path) override;
|
||||||
|
|
||||||
|
bool remove(const std::string &str) override;
|
||||||
|
bool move_yield(const std::string &dst, const std::string &src) override;
|
||||||
|
bool move_replace(const std::string &dst, const std::string &src) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string base;
|
||||||
|
|
||||||
|
struct Handler
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
std::function<void (const FileNotifyInfo &)> func;
|
||||||
|
HANDLE handle = nullptr;
|
||||||
|
HANDLE event = nullptr;
|
||||||
|
DWORD async_buffer[1024];
|
||||||
|
OVERLAPPED overlapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<FileNotifyHandle, Handler> handlers;
|
||||||
|
FileNotifyHandle handle_id = 0;
|
||||||
|
void kick_async(Handler &handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
add_granite_internal_lib(granite-math
|
||||||
|
math.hpp math.cpp
|
||||||
|
frustum.hpp frustum.cpp
|
||||||
|
aabb.cpp aabb.hpp
|
||||||
|
render_parameters.hpp
|
||||||
|
interpolation.cpp interpolation.hpp
|
||||||
|
muglm/muglm.cpp muglm/muglm.hpp
|
||||||
|
muglm/muglm_impl.hpp muglm/matrix_helper.hpp
|
||||||
|
transforms.cpp transforms.hpp
|
||||||
|
simd.hpp simd_headers.hpp)
|
||||||
|
|
||||||
|
target_include_directories(granite-math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/* 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 "aabb.hpp"
|
||||||
|
#include <float.h>
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
AABB AABB::transform(const mat4 &m) const
|
||||||
|
{
|
||||||
|
vec3 m0 = vec3(FLT_MAX);
|
||||||
|
vec3 m1 = vec3(-FLT_MAX);
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
vec3 c = get_corner(i);
|
||||||
|
vec4 t = m * vec4(c, 1.0f);
|
||||||
|
vec3 v = t.xyz();
|
||||||
|
m0 = min(v, m0);
|
||||||
|
m1 = max(v, m1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AABB(m0, m1);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 AABB::get_coord(float dx, float dy, float dz) const
|
||||||
|
{
|
||||||
|
return mix(minimum.v3, maximum.v3, vec3(dx, dy, dz));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AABB::expand(const AABB &aabb)
|
||||||
|
{
|
||||||
|
minimum.v3 = min(minimum.v3, aabb.minimum.v3);
|
||||||
|
maximum.v3 = max(maximum.v3, aabb.maximum.v3);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/* 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 "math.hpp"
|
||||||
|
#include "muglm/muglm_impl.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class AABB
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AABB(vec3 minimum_, vec3 maximum_)
|
||||||
|
{
|
||||||
|
minimum.v4 = vec4(minimum_, 1.0f);
|
||||||
|
maximum.v4 = vec4(maximum_, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
AABB() = default;
|
||||||
|
|
||||||
|
vec3 get_coord(float dx, float dy, float dz) const;
|
||||||
|
AABB transform(const mat4 &m) const;
|
||||||
|
|
||||||
|
void expand(const AABB &aabb);
|
||||||
|
|
||||||
|
const vec3 &get_minimum() const
|
||||||
|
{
|
||||||
|
return minimum.v3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vec3 &get_maximum() const
|
||||||
|
{
|
||||||
|
return maximum.v3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vec4 &get_minimum4() const
|
||||||
|
{
|
||||||
|
return minimum.v4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vec4 &get_maximum4() const
|
||||||
|
{
|
||||||
|
return maximum.v4;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 &get_minimum4()
|
||||||
|
{
|
||||||
|
return minimum.v4;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 &get_maximum4()
|
||||||
|
{
|
||||||
|
return maximum.v4;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 get_corner(unsigned i) const
|
||||||
|
{
|
||||||
|
float x = i & 1 ? maximum.v3.x : minimum.v3.x;
|
||||||
|
float y = i & 2 ? maximum.v3.y : minimum.v3.y;
|
||||||
|
float z = i & 4 ? maximum.v3.z : minimum.v3.z;
|
||||||
|
return vec3(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 get_center() const
|
||||||
|
{
|
||||||
|
return minimum.v3 + (maximum.v3 - minimum.v3) * vec3(0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_radius() const
|
||||||
|
{
|
||||||
|
return 0.5f * distance(minimum.v3, maximum.v3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
union
|
||||||
|
{
|
||||||
|
vec3 v3;
|
||||||
|
vec4 v4;
|
||||||
|
} minimum, maximum;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
# Modified SQUAD for non-uniform timestamps
|
||||||
|
|
||||||
|
The SQUAD algorithm is a well-known algorithm for smooth interpolation of rotations.
|
||||||
|
|
||||||
|
The standard and simple algorithm for rotation is SLERP,
|
||||||
|
which ensures constant angular velocity over a given interpolation segment.
|
||||||
|
However, the flaw of SLERP for camera interpolation is that the angular velocity
|
||||||
|
is not continuous, and it will abruptly change on a new segment.
|
||||||
|
This problem is solved by SQUAD, but in its naive implementation, the length of
|
||||||
|
each segment must be uniform, otherwise the derivation fails.
|
||||||
|
|
||||||
|
I spent some time studying the underlying math and derived a formula that works for
|
||||||
|
non-uniform timestamps as well.
|
||||||
|
|
||||||
|
## The standard runtime algorithm
|
||||||
|
|
||||||
|
In SQUAD, each key-frame point is represented as
|
||||||
|
a quaternion q<sub>k</sub> at timestamp t<sub>k</sub>.
|
||||||
|
At each timestamp, we also pre-compute a
|
||||||
|
helper control point q<sup>c</sup><sub>k</sub>,
|
||||||
|
which derivation will be explored further below.
|
||||||
|
|
||||||
|
We are given the implementation:
|
||||||
|
|
||||||
|
squad<sub>k</sub>(t) = slerp(slerp(q<sub>k</sub>, q<sub>k+1</sub>, t),
|
||||||
|
slerp(q<sup>c</sup><sub>k</sub>, q<sup>c</sup><sub>k+1</sub>, t),
|
||||||
|
2t(1 - t))
|
||||||
|
|
||||||
|
t is given here in the range [0, 1), and is computed by:
|
||||||
|
|
||||||
|
t = (T - t<sub>k</sub>) / (t<sub>k+1</sub> - t<sub>k</sub>)
|
||||||
|
|
||||||
|
where T is the global time for which to evaluate.
|
||||||
|
An animation clip is stitched together by many such splines, one for each k.
|
||||||
|
|
||||||
|
### Analyze the expression
|
||||||
|
|
||||||
|
To perform further calculus on the squad(t) function, we can simplify to scalars.
|
||||||
|
If we assume for the purposes of analysis that all
|
||||||
|
the rotations have the same axis of rotation, we can
|
||||||
|
rewrite squad(t) to a linear interpolation of rotation angle θ, which
|
||||||
|
each key-frame now represents:
|
||||||
|
|
||||||
|
squad<sub>k</sub>(t) = lerp(lerp(θ<sub>k</sub>, θ<sub>k+1</sub>, t),
|
||||||
|
lerp(θ<sup>c</sup><sub>k</sub>, θ<sup>c</sup><sub>k+1</sub>, t),
|
||||||
|
2t(1 - t))
|
||||||
|
|
||||||
|
All these lerps are trivial expressions:
|
||||||
|
|
||||||
|
lerp(a, b, t) = (1 - t)a + tb
|
||||||
|
|
||||||
|
We can expand the expression and compute their first and second order derivatives.
|
||||||
|
|
||||||
|
v<sub>k</sub>(t) =
|
||||||
|
(-3 + 8t - 6t<sup>2</sup>) θ<sub>k</sub> +
|
||||||
|
(-1 - 4t + 6t<sup>2</sup>) θ<sub>k+1</sub> +
|
||||||
|
(2 - 8t + 6t<sup>2</sup>) θ<sup>c</sup><sub>k</sub> +
|
||||||
|
(4t - 6t<sup>2</sup>) θ<sup>c</sup><sub>k+1</sub>
|
||||||
|
|
||||||
|
a<sub>k</sub>(t) =
|
||||||
|
(8 - 12t) θ<sub>k</sub> +
|
||||||
|
(-4 + 12t) θ<sub>k+1</sub> +
|
||||||
|
(-8 + 12t) θ<sup>c</sup><sub>k</sub> +
|
||||||
|
(4 - 12t) θ<sup>c</sup><sub>k+1</sub>
|
||||||
|
|
||||||
|
It is important to note here that we derive with respect to the spline local parameter t.
|
||||||
|
To obtain the absolute angular velocity and acceleration at time t, we need to apply chain rules:
|
||||||
|
|
||||||
|
V<sub>k</sub>(t) = v<sub>k</sub>(t) (dt / dT) = v<sub>k</sub>(t) / d<sub>k</sub>
|
||||||
|
|
||||||
|
where d<sub>k</sub> = t<sub>k+1</sub> - t<sub>k</sub>,
|
||||||
|
and V<sub>k</sub>(t) is absolute angular velocity, dθ / dT.
|
||||||
|
|
||||||
|
Similarly, A<sub>k</sub>(t) = a<sub>k</sub>(t) / d<sub>k</sub><sup>2</sup> is
|
||||||
|
absolute angular acceleration, d<sup>2</sup>θ / (dT)<sup>2</sup>. When d<sub>k</sub> is constant,
|
||||||
|
all uses of d<sub>k</sub> cancel out,
|
||||||
|
and this is the assumption various algorithms online make.
|
||||||
|
To ensure first order continuity we must satisfy
|
||||||
|
V<sub>k</sub>(1) = V<sub>k+1</sub>(0), or alternatively if d<sub>k</sub> is constant,
|
||||||
|
v<sub>k</sub>(1) = v<sub>k+1</sub>(0). Similarly, if we want to ensure continuous second order derivative, we must
|
||||||
|
satisfy A<sub>k</sub>(1) = A<sub>k+1</sub>(0).
|
||||||
|
|
||||||
|
**v<sub>k+1</sub>(0)** =
|
||||||
|
-3θ<sub>k+1</sub> +
|
||||||
|
1θ<sub>k+2</sub> +
|
||||||
|
2θ<sup>c</sup><sub>k+1</sub> +
|
||||||
|
0θ<sup>c</sup><sub>k+2</sub> =\
|
||||||
|
**(θ<sub>k+2</sub> - θ<sub>k+1</sub>) -
|
||||||
|
2(θ<sub>k+1</sub> - θ<sup>c</sup><sub>k+1</sub>)**
|
||||||
|
|
||||||
|
**v<sub>k</sub>(1)** =
|
||||||
|
-1θ<sub>k</sub> +
|
||||||
|
3θ<sub>k+1</sub> +
|
||||||
|
0θ<sup>c</sup><sub>k</sub> -
|
||||||
|
2θ<sup>c</sup><sub>k+1</sub> =\
|
||||||
|
**(θ<sub>k+1</sub> - θ<sub>k</sub>) +
|
||||||
|
2(θ<sub>k+1</sub> - θ<sup>c</sup><sub>k+1</sub>)**
|
||||||
|
|
||||||
|
**a<sub>k+1</sub>(0)** =
|
||||||
|
8θ<sub>k+1</sub> -
|
||||||
|
4θ<sub>k+2</sub> -
|
||||||
|
8θ<sup>c</sup><sub>k+1</sub> +
|
||||||
|
4θ<sup>c</sup><sub>k+2</sub> =\
|
||||||
|
**8(θ<sub>k+1</sub> - θ<sup>c</sup><sub>k+1</sub>) -
|
||||||
|
4(θ<sub>k+2</sub> - θ<sup>c</sup><sub>k+2</sub>)**
|
||||||
|
|
||||||
|
**a<sub>k</sub>(1)** =
|
||||||
|
-4θ<sub>k</sub> +
|
||||||
|
8θ<sub>k+1</sub> +
|
||||||
|
4θ<sup>c</sup><sub>k</sub> -
|
||||||
|
8θ<sup>c</sup><sub>k+1</sub> =\
|
||||||
|
**8(θ<sub>k+1</sub> - θ<sup>c</sup><sub>k+1</sub>) -
|
||||||
|
4(θ<sub>k</sub> - θ<sup>c</sup><sub>k</sub>)**
|
||||||
|
|
||||||
|
Based on these expressions, we can already intuit what the relationship
|
||||||
|
between the control points and the key-frame points are. The difference expresses
|
||||||
|
acceleration. With positive acceleration, the control points lags behind the key-frame, and vice versa.
|
||||||
|
Looking at the velocity expressions, with positive acceleration, we also get larger velocity at t = 1 compared to t = 0,
|
||||||
|
as expected.
|
||||||
|
|
||||||
|
To satisfy the velocity equations, we need to choose v<sub>k+1</sub>(0) = v<sub>k</sub>(1), so\
|
||||||
|
(θ<sub>k+2</sub> - θ<sub>k+1</sub>) -
|
||||||
|
2(θ<sub>k+1</sub> - θ<sup>c</sup><sub>k+1</sub>) =
|
||||||
|
(θ<sub>k+1</sub> - θ<sub>k</sub>) +
|
||||||
|
2(θ<sub>k+1</sub> - θ<sup>c</sup><sub>k+1</sub>)\
|
||||||
|
(θ<sub>k+2</sub> - θ<sub>k+1</sub>) -
|
||||||
|
(θ<sub>k+1</sub> - θ<sub>k</sub>) =
|
||||||
|
4(θ<sub>k+1</sub> - θ<sup>c</sup><sub>k+1</sub>)\
|
||||||
|
((θ<sub>k+2</sub> - θ<sub>k+1</sub>) -
|
||||||
|
(θ<sub>k+1</sub> - θ<sub>k</sub>)) / 4 =
|
||||||
|
θ<sub>k+1</sub> - θ<sup>c</sup><sub>k+1</sub>\
|
||||||
|
θ<sub>k+1</sub> - ((θ<sub>k+2</sub> - θ<sub>k+1</sub>) -
|
||||||
|
(θ<sub>k+1</sub> - θ<sub>k</sub>)) / 4 =
|
||||||
|
**θ<sup>c</sup><sub>k+1</sub>**
|
||||||
|
|
||||||
|
(θ<sub>k+2</sub> - θ<sub>k+1</sub>) - (θ<sub>k+1</sub> - θ<sub>k</sub>)
|
||||||
|
is quite recognizable and intuitive.
|
||||||
|
This is the discrete measurement of acceleration at t<sub>k+1</sub>.
|
||||||
|
|
||||||
|
For simplicity of notation, we introduce the local delta,
|
||||||
|
Δ<sub>k</sub> = θ<sub>k</sub> - θ<sup>c</sup><sub>k</sub>.
|
||||||
|
We can now rewrite the equations in a more digestable form:
|
||||||
|
|
||||||
|
v<sub>k+1</sub>(0) =
|
||||||
|
(θ<sub>k+2</sub> - θ<sub>k+1</sub>) - 2Δ<sub>k+1</sub>
|
||||||
|
|
||||||
|
v<sub>k</sub>(1) =
|
||||||
|
(θ<sub>k+1</sub> - θ<sub>k</sub>) + 2Δ<sub>k+1</sub>
|
||||||
|
|
||||||
|
a<sub>k+1</sub>(0) =
|
||||||
|
8Δ<sub>k+1</sub> - 4Δ<sub>k+2</sub>
|
||||||
|
|
||||||
|
a<sub>k</sub>(1) =
|
||||||
|
8Δ<sub>k+1</sub> - 4Δ<sub>k</sub>
|
||||||
|
|
||||||
|
This equation will only yield a continuous acceleration if
|
||||||
|
Δ<sub>k</sub> = Δ<sub>k+2</sub>, which is not guaranteed.
|
||||||
|
However, we have the nice property that a constant acceleration will
|
||||||
|
yield a constant a<sub>k</sub>(t) for any k equal to 4Δ<sub>k</sub>.
|
||||||
|
As we deduced earlier, Δ<sub>k</sub> is 1/4th the measured discrete acceleration,
|
||||||
|
so everything checks out. Continuous acceleration is a nice property,
|
||||||
|
but not required for smooth camera motion.
|
||||||
|
|
||||||
|
## Going back to the quaternion domain
|
||||||
|
|
||||||
|
We have found expressions for the control points, but the derivation
|
||||||
|
has been happening in the angular domain, we need to work with quaternions.
|
||||||
|
|
||||||
|
Here, articles online will usually begin talking about logarithms and exponential functions of quaternions
|
||||||
|
which at first glance is pure non-sense, but it is actually fairly intuitive.
|
||||||
|
It took me a while to understand what the hell the article authors were smoking at first.
|
||||||
|
|
||||||
|
The insight is that **multiplying** two quaternions **adds** their rotational angles.
|
||||||
|
This is exactly the same as complex numbers, where multiplying two complex numbers
|
||||||
|
add their angles.
|
||||||
|
For logarithms of quaternions to work, we need to convert them to a number where
|
||||||
|
adding the results will function similarly to angular addition. Taking the exponent
|
||||||
|
should give us back the result.
|
||||||
|
|
||||||
|
q<sub>a</sub>q<sub>b</sub> = exp(ln(q<sub>a</sub>q<sub>b</sub>)) =
|
||||||
|
exp(ln(q<sub>a</sub>) + ln(q<sub>b</sub>))
|
||||||
|
|
||||||
|
As an aside, this extension also allows us to reason about powers of quaternions, since
|
||||||
|
ln(q<sub>a</sub><sup>c</sup>) = c⋅ln(q<sub>a</sub>).
|
||||||
|
|
||||||
|
The logarithm for a unit quaternion is computed as:
|
||||||
|
|
||||||
|
```
|
||||||
|
// vec3 quat_log(q)
|
||||||
|
|
||||||
|
if (abs(q.w) > 0.9999f)
|
||||||
|
return vec3(0.0f);
|
||||||
|
else
|
||||||
|
return normalize(q.as_vec4().xyz()) * acos(q.w);
|
||||||
|
```
|
||||||
|
|
||||||
|
The main confusion for me here is that quaternion multiplication does not commute,
|
||||||
|
but here the logarithm additions do. Subtraction might make more sense ...
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// compute_inner_control_point_delta(q, delta)
|
||||||
|
|
||||||
|
quat inv_q1 = conjugate(q1);
|
||||||
|
quat delta_k = inv_q1 * q2; // q2 - q1
|
||||||
|
quat delta_k_minus1 = inv_q1 * q0; // q0 - q1 = -(q1 - q0)
|
||||||
|
vec3 delta_k_log = quat_log(delta_k);
|
||||||
|
vec3 delta_k_minus1_log = quat_log(delta_k_minus1);
|
||||||
|
vec3 delta = 0.25f * (delta_k_log + delta_k_minus1_log);
|
||||||
|
return delta;
|
||||||
|
```
|
||||||
|
|
||||||
|
The multiplication order seems somewhat arbitrary,
|
||||||
|
and I cannot prove exactly why we have to do it like this, but I cribbed
|
||||||
|
this part from the web. This document so far is trying to justify how it works.
|
||||||
|
At the very least, we can see similarities with the original derivation.
|
||||||
|
Here, `delta_k` and `delta_k_minus1` measure the velocities between key-frames.
|
||||||
|
Multiplying is "addition", but multiplying by conjugate is "subtraction".
|
||||||
|
By subtracting in the log-domain we can get an angular differential and measure acceleration.
|
||||||
|
As expected, we also take 1/4th since the delta is 1/4th measured acceleration.
|
||||||
|
|
||||||
|
This delta is later used to construct the control point q<sup>c</sup><sub>k</sub>.
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// compute_inner_control_point(q, delta)
|
||||||
|
|
||||||
|
// Subtraction in angular domain.
|
||||||
|
return q * quat_exp(-delta);
|
||||||
|
```
|
||||||
|
|
||||||
|
which maps to the definition we made:
|
||||||
|
|
||||||
|
θ<sub>k</sub> - θ<sup>c</sup><sub>k</sub> =
|
||||||
|
Δ<sub>k</sub> \
|
||||||
|
**θ<sup>c</sup><sub>k</sub> =
|
||||||
|
θ<sub>k</sub> - Δ<sub>k</sub>**
|
||||||
|
|
||||||
|
To my great surprise, this is actually delightfully simple.
|
||||||
|
The logarithm is a vec3 where the direction is axis of rotation,
|
||||||
|
and length is the angle θ. This is what allows us to add/subtract the rotations
|
||||||
|
together. This style of expressing rotation is basically how we would
|
||||||
|
express torque in physics, nice!
|
||||||
|
|
||||||
|
The exponent just inverts what the log did.
|
||||||
|
We recover the angle by taking length of vector
|
||||||
|
and rebuilding the quaternion from that.
|
||||||
|
|
||||||
|
```
|
||||||
|
// quat quat_exp(q)
|
||||||
|
|
||||||
|
float l = dot(q, q);
|
||||||
|
if (l < 0.000001f)
|
||||||
|
{
|
||||||
|
return quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float vlen = length(q);
|
||||||
|
vec3 v = normalize(q) * sin(vlen);
|
||||||
|
return quat(cos(vlen), v);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-uniform time deltas d<sub>k</sub>
|
||||||
|
|
||||||
|
This is where it gets spicy and where I was initially stumped when
|
||||||
|
attempting to implement SQUAD. When constructing arbitrary camera paths,
|
||||||
|
it is helpful to be able to place key-frames at any timestamp.
|
||||||
|
What unfortunately happens now is that velocities are no longer
|
||||||
|
continuous over a spline boundary, because the splines are now swept at varying
|
||||||
|
rates. We will need to re-derive the control points,
|
||||||
|
based on V<sub>k</sub>(t), not v<sub>k</sub>(t), in angular domain.
|
||||||
|
|
||||||
|
V<sub>k+1</sub>(0) =
|
||||||
|
((θ<sub>k+2</sub> - θ<sub>k+1</sub>) - 2Δ<sub>k+1</sub>) / d<sub>k+1</sub>
|
||||||
|
|
||||||
|
V<sub>k</sub>(1) =
|
||||||
|
((θ<sub>k+1</sub> - θ<sub>k</sub>) + 2Δ<sub>k+1</sub>) / d<sub>k</sub>
|
||||||
|
|
||||||
|
To be able to solve this, we need to consider that Δ<sub>k+1</sub> need
|
||||||
|
not be a single value.
|
||||||
|
When evaluating spline k and k + 1, it can take different values as needed.
|
||||||
|
This gives rise to the "incoming" and "outgoing" control points.
|
||||||
|
The incoming control point delta is used when evaluating the previous spline.
|
||||||
|
|
||||||
|
V<sub>k+1</sub>(0) =
|
||||||
|
((θ<sub>k+2</sub> - θ<sub>k+1</sub>) - 2Δ<sup>o</sup><sub>k+1</sub>) / d<sub>k+1</sub>
|
||||||
|
|
||||||
|
V<sub>k</sub>(1) =
|
||||||
|
((θ<sub>k+1</sub> - θ<sub>k</sub>) + 2Δ<sup>i</sup><sub>k+1</sub>) / d<sub>k</sub>
|
||||||
|
|
||||||
|
The superscript o and i denote outgoing and incoming respectively.
|
||||||
|
We can now solve this equation directly, similar to the derivation we did for constant d<sub>k</sub> earlier.
|
||||||
|
|
||||||
|
((θ<sub>k+2</sub> - θ<sub>k+1</sub>) - 2Δ<sup>o</sup><sub>k+1</sub>) / d<sub>k+1</sub> =
|
||||||
|
((θ<sub>k+1</sub> - θ<sub>k</sub>) + 2Δ<sup>i</sup><sub>k+1</sub>) / d<sub>k</sub>
|
||||||
|
|
||||||
|
(θ<sub>k+2</sub> - θ<sub>k+1</sub>) / d<sub>k+1</sub> -
|
||||||
|
2Δ<sup>o</sup><sub>k+1</sub> / d<sub>k+1</sub> =
|
||||||
|
(θ<sub>k+1</sub> - θ<sub>k</sub>) / d<sub>k</sub> +
|
||||||
|
2Δ<sup>i</sup><sub>k+1</sub> / d<sub>k</sub>
|
||||||
|
|
||||||
|
(θ<sub>k+2</sub> - θ<sub>k+1</sub>) / d<sub>k+1</sub> -
|
||||||
|
(θ<sub>k+1</sub> - θ<sub>k</sub>) / d<sub>k</sub> -
|
||||||
|
2Δ<sup>o</sup><sub>k+1</sub> / d<sub>k+1</sub> =
|
||||||
|
2Δ<sup>i</sup><sub>k+1</sub> / d<sub>k</sub>
|
||||||
|
|
||||||
|
If we let the ratio r be d<sub>k</sub> / d<sub>k+1</sub>, we get
|
||||||
|
|
||||||
|
Δ<sup>i</sup><sub>k+1</sub> =
|
||||||
|
((θ<sub>k+2</sub> - θ<sub>k+1</sub>)(d<sub>k</sub> / d<sub>k+1</sub>) -
|
||||||
|
(θ<sub>k+1</sub> - θ<sub>k</sub>)) / 2 -
|
||||||
|
Δ<sup>o</sup><sub>k+1</sub>(d<sub>k</sub> / d<sub>k+1</sub>)
|
||||||
|
|
||||||
|
Which shows that we can actually select the outgoing control point rather freely,
|
||||||
|
and we can then use this formula to compensate the difference in
|
||||||
|
d<sub>k</sub> in the incoming control point.
|
||||||
|
|
||||||
|
For the outgoing control point, we should modify the acceleration
|
||||||
|
computation to be aware of different step rates. Basically,
|
||||||
|
we normalize the discrete velocities in terms of global time T.
|
||||||
|
There might be better ways of computing this, but, meh.
|
||||||
|
From empiric testing, the result is pretty accurate.
|
||||||
|
|
||||||
|
```
|
||||||
|
quat inv_q1 = conjugate(q1);
|
||||||
|
quat delta_k = inv_q1 * q2; // q2 - q1
|
||||||
|
quat delta_k_minus1 = inv_q1 * q0; // q0 - q1 = -(q1 - q0)
|
||||||
|
vec3 delta_k_log = quat_log(delta_k);
|
||||||
|
vec3 delta_k_minus1_log = quat_log(delta_k_minus1);
|
||||||
|
|
||||||
|
// We sample velocity at the center of the segment when taking the difference.
|
||||||
|
// Future sample is at t = +1/2 dt
|
||||||
|
// Past sample is at t = -1/2 dt
|
||||||
|
float segment_time = 0.5f * (dt0 + dt1);
|
||||||
|
vec3 absolute_accel = (delta_k_log / dt1 + delta_k_minus1_log / dt0) / segment_time;
|
||||||
|
vec3 delta = (0.25f * dt1 * dt1) * absolute_accel;
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
// Computed from snippet above
|
||||||
|
vec3 outgoing = tmp_spline_deltas[i];
|
||||||
|
|
||||||
|
float dt0 = new_linear_timestamps[i] - new_linear_timestamps[i - 1];
|
||||||
|
float dt1 = i + 1 < n ? (new_linear_timestamps[i + 1] - new_linear_timestamps[i]) : dt0;
|
||||||
|
float t_ratio = dt0 / dt1;
|
||||||
|
|
||||||
|
const quat &q0 = new_linear_values[i - 1];
|
||||||
|
const quat &q1 = new_linear_values[i];
|
||||||
|
const quat &q2 = i + 1 < n ? new_linear_values[i + 1] : q1;
|
||||||
|
|
||||||
|
quat q12 = conjugate(q1) * q2;
|
||||||
|
quat q10 = conjugate(q1) * q0; // This is implicitly negated.
|
||||||
|
vec3 delta_q12 = quat_log(q12);
|
||||||
|
vec3 delta_q10 = quat_log(q10);
|
||||||
|
|
||||||
|
vec3 incoming = 0.5f * (t_ratio * delta_q12 + delta_q10) - t_ratio * outgoing;
|
||||||
|
|
||||||
|
spline_data[3 * spline + 0] = q1 * quat_exp(-incoming);
|
||||||
|
spline_data[3 * spline + 1] = q1;
|
||||||
|
spline_data[3 * spline + 2] = q1 * quat_exp(-outgoing);
|
||||||
|
```
|
||||||
|
|
||||||
|
Each key-frame gets 3 values. This is very similar to the
|
||||||
|
CUBICSPLINE formulation used in glTF.
|
||||||
|
When evalulating the spline in runtime we look at indices
|
||||||
|
3 * k + {1, 2, 3, 4}.
|
||||||
|
|
||||||
|
### Modified SQUAD function
|
||||||
|
|
||||||
|
squad<sub>k</sub>(t) = slerp(slerp(q<sub>k</sub>, q<sub>k+1</sub>, t),
|
||||||
|
slerp(q<sub>k</sub>·quatExp(-Δ<sup>o</sup><sub>k</sub>),
|
||||||
|
q<sub>k+1</sub>·quatExp(-Δ<sup>i</sup><sub>k+1</sub>), t),
|
||||||
|
2t(1 - t))
|
||||||
|
|
||||||
|
Δ is in the log domain as we computed above with outgoing and incoming deltas.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
While doing this work, I also made a test bench of sorts to evaluate the results.
|
||||||
|
I tested 4 different scenarios with scalars.
|
||||||
|
|
||||||
|
- Interpolate quadratic function with even timestamps. Should be 100% exact.
|
||||||
|
- Interpolate quadratic function with uneven timestamps. Will have some error.
|
||||||
|
- Interpolate cubic function with even timestamps. Expect some errors due to non-constant acceleration.
|
||||||
|
- Interpolate cubic function with uneven timestamps. Expect some errors due to non-constant acceleration.
|
||||||
|
|
||||||
|
We want to validate:
|
||||||
|
- Average error of reference function f(t) and interpolated result.
|
||||||
|
- Continuity of measured first derivative (and second derivative).
|
||||||
|
|
||||||
|
### Quadratic
|
||||||
|
|
||||||
|
f(t) = 0.5t - 0.25t<sup>2</sup>
|
||||||
|
|
||||||
|
#### Even timestamps
|
||||||
|
|
||||||
|
Key frames placed at t = {0, 0.5, 1.0, 2.0, 2.5, 3.0}.\
|
||||||
|
Perfect result. As expected.
|
||||||
|
|
||||||
|
#### Uneven timestamps
|
||||||
|
Key frames placed at t = {0, 1.0, 1.8, 2.1, 2.9, 3.0, 4.2, 4.3, 5.0, 6.0}.\
|
||||||
|
Average error: 0.008141\
|
||||||
|
Continuous first derivative, discontinuous second derivative.
|
||||||
|
|
||||||
|
### Cubic
|
||||||
|
|
||||||
|
f(t) = 0.5t - 0.25t<sup>2</sup> + 0.25t<sup>3</sup>
|
||||||
|
|
||||||
|
#### Even timestamps
|
||||||
|
Key frames placed at t = {0, 0.5, 1.0, 2.0, 2.5, 3.0}.\
|
||||||
|
Average error: 0.00195\
|
||||||
|
Continuous first derivative, discontinuous second derivative.
|
||||||
|
|
||||||
|
#### Uneven timestamps
|
||||||
|
Key frames placed at t = {0, 0.5, 0.9, 1.1, 1.4, 1.5, 2.1, 2.2, 2.5, 3.0}.\
|
||||||
|
Average error: 0.008285\
|
||||||
|
Continuous first derivative, discontinuous second derivative.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
The more even timestamps we have, the more accurate the spline becomes.
|
||||||
|
The error is also quite acceptable, and we see continuous first derivative,
|
||||||
|
which is the critical part to get right.
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/* 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 "frustum.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
|
||||||
|
// For reference, should always use SIMD-version.
|
||||||
|
bool Frustum::intersects_slow(const AABB &aabb) const
|
||||||
|
{
|
||||||
|
for (auto &plane : planes)
|
||||||
|
{
|
||||||
|
bool intersects_plane = false;
|
||||||
|
for (unsigned i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (dot(vec4(aabb.get_corner(i), 1.0f), plane) >= 0.0f)
|
||||||
|
{
|
||||||
|
intersects_plane = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intersects_plane)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Frustum::intersects_sphere(const AABB &aabb) const
|
||||||
|
{
|
||||||
|
vec4 center(aabb.get_center(), 1.0f);
|
||||||
|
float radius = aabb.get_radius();
|
||||||
|
|
||||||
|
for (auto &plane : planes)
|
||||||
|
if (dot(plane, center) < -radius)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr float FarClipInfiniteClamp = 1e-10f;
|
||||||
|
|
||||||
|
vec3 Frustum::get_coord(float dx, float dy, float dz) const
|
||||||
|
{
|
||||||
|
dz = 1.0f - dz;
|
||||||
|
|
||||||
|
bool infinite_z = inv_view_projection[3][3] == 0.0f;
|
||||||
|
if (infinite_z)
|
||||||
|
dz = muglm::max<float>(dz, FarClipInfiniteClamp);
|
||||||
|
|
||||||
|
vec4 clip = vec4(2.0f * dx - 1.0f, 2.0f * dy - 1.0f, dz, 1.0f);
|
||||||
|
clip = inv_view_projection * clip;
|
||||||
|
return clip.xyz() / clip.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 Frustum::get_bounding_sphere(const mat4 &inv_projection, const mat4 &inv_view)
|
||||||
|
{
|
||||||
|
// Make sure that radius is numerically stable throughout, since we use that as a snapping factor potentially.
|
||||||
|
// Use the inverse projection to create the radius.
|
||||||
|
|
||||||
|
const auto get_coord = [&](float x, float y, float z) -> vec3 {
|
||||||
|
vec4 clip = vec4(x, y, z, 1.0f);
|
||||||
|
clip = inv_projection * clip;
|
||||||
|
return clip.xyz() / clip.w;
|
||||||
|
};
|
||||||
|
|
||||||
|
vec3 center_near = get_coord(0.0f, 0.0f, 0.0f);
|
||||||
|
vec3 center_far = get_coord(0.0f, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
vec3 near_pos = get_coord(-1.0f, -1.0f, 0.0f);
|
||||||
|
vec3 far_pos = get_coord(+1.0f, +1.0f, 1.0f);
|
||||||
|
|
||||||
|
float C = length(center_far - center_near);
|
||||||
|
float N = dot(near_pos - center_near, near_pos - center_near);
|
||||||
|
float F = dot(far_pos - center_far, far_pos - center_far);
|
||||||
|
|
||||||
|
// Solve the equation:
|
||||||
|
// n^2 + x^2 == f^2 + (C - x)^2 =>
|
||||||
|
// N + x^2 == F + C^2 - 2Cx + x^2.
|
||||||
|
// x = (F - N + C^2) / 2C
|
||||||
|
float center_distance = (F - N + C * C) / (2.0f * C);
|
||||||
|
float radius = muglm::sqrt(center_distance * center_distance + N);
|
||||||
|
vec3 view_space_center = center_near + center_distance * normalize(center_far - center_near);
|
||||||
|
vec3 center = (inv_view * vec4(view_space_center, 1.0f)).xyz();
|
||||||
|
return vec4(center, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Frustum::build_planes(const mat4 &inv_view_projection_)
|
||||||
|
{
|
||||||
|
inv_view_projection = inv_view_projection_;
|
||||||
|
bool infinite_z = inv_view_projection[3][3] == 0.0f;
|
||||||
|
float far_clip_z = infinite_z ? FarClipInfiniteClamp : 0.0f;
|
||||||
|
const vec4 tln(-1.0f, -1.0f, 1.0f, 1.0f);
|
||||||
|
const vec4 bln(-1.0f, +1.0f, 1.0f, 1.0f);
|
||||||
|
const vec4 blf(-1.0f, +1.0f, far_clip_z, 1.0f);
|
||||||
|
const vec4 trn(+1.0f, -1.0f, 1.0f, 1.0f);
|
||||||
|
const vec4 trf(+1.0f, -1.0f, far_clip_z, 1.0f);
|
||||||
|
const vec4 brn(+1.0f, +1.0f, 1.0f, 1.0f);
|
||||||
|
const vec4 brf(+1.0f, +1.0f, far_clip_z, 1.0f);
|
||||||
|
const vec4 c(0.0f, 0.0f, 0.5f, 1.0f);
|
||||||
|
|
||||||
|
const auto project = [](const vec4 &v) {
|
||||||
|
return v.xyz() / vec3(v.w);
|
||||||
|
};
|
||||||
|
|
||||||
|
vec3 TLN = project(inv_view_projection * tln);
|
||||||
|
vec3 BLN = project(inv_view_projection * bln);
|
||||||
|
vec3 BLF = project(inv_view_projection * blf);
|
||||||
|
vec3 TRN = project(inv_view_projection * trn);
|
||||||
|
vec3 TRF = project(inv_view_projection * trf);
|
||||||
|
vec3 BRN = project(inv_view_projection * brn);
|
||||||
|
vec3 BRF = project(inv_view_projection * brf);
|
||||||
|
vec4 center = inv_view_projection * c;
|
||||||
|
|
||||||
|
vec3 l = normalize(cross(BLF - BLN, TLN - BLN));
|
||||||
|
vec3 r = normalize(cross(TRF - TRN, BRN - TRN));
|
||||||
|
vec3 n = normalize(cross(BLN - BRN, TRN - BRN));
|
||||||
|
vec3 f = normalize(cross(TRF - BRF, BLF - BRF));
|
||||||
|
vec3 t = normalize(cross(TLN - TRN, TRF - TRN));
|
||||||
|
vec3 b = normalize(cross(BRF - BRN, BLN - BRN));
|
||||||
|
|
||||||
|
planes[0] = vec4(l, -dot(l, BLN));
|
||||||
|
planes[1] = vec4(r, -dot(r, TRN));
|
||||||
|
planes[2] = vec4(n, -dot(n, BRN));
|
||||||
|
planes[3] = infinite_z ? vec4(0.0f) : vec4(f, -dot(f, BRF));
|
||||||
|
planes[4] = vec4(t, -dot(t, TRN));
|
||||||
|
planes[5] = vec4(b, -dot(b, BRN));
|
||||||
|
|
||||||
|
// Winding order checks.
|
||||||
|
for (auto &p : planes)
|
||||||
|
if (dot(center, p) < 0.0f)
|
||||||
|
p = -p;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/* 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 "math.hpp"
|
||||||
|
#include "aabb.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
class Frustum
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void build_planes(const mat4& inv_view_projection);
|
||||||
|
bool intersects_sphere(const AABB &aabb) const;
|
||||||
|
bool intersects_slow(const AABB &aabb) const;
|
||||||
|
|
||||||
|
vec3 get_coord(float dx, float dy, float dz) const;
|
||||||
|
|
||||||
|
static vec4 get_bounding_sphere(const mat4 &inv_projection, const mat4 &inv_view);
|
||||||
|
|
||||||
|
const vec4 *get_planes() const
|
||||||
|
{
|
||||||
|
return planes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
vec4 planes[6];
|
||||||
|
mat4 inv_view_projection;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "interpolation.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
float catmull_rom_spline(float c0, float c1, float c2, float c3, float phase)
|
||||||
|
{
|
||||||
|
float phase2 = phase * phase;
|
||||||
|
float phase3 = phase2 * phase;
|
||||||
|
return c1 +
|
||||||
|
(c2 - c0) * 0.5f * phase +
|
||||||
|
(c0 - (2.5f * c1) + (2.0f * c2) - (0.5f * c3)) * phase2 +
|
||||||
|
((-0.5f * c0) + (1.5f * c1) - (1.5f * c2) + (0.5f * c3)) * phase3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes the analytic derivative of the spline dFd(phase).
|
||||||
|
float catmull_rom_spline_gradient(float c0, float c1, float c2, float c3, float phase)
|
||||||
|
{
|
||||||
|
float phase2 = phase * phase;
|
||||||
|
return (c2 - c0) * 0.5f +
|
||||||
|
(c0 - (2.5f * c1) + (2.0f * c2) - (0.5f * c3)) * 2.0f * phase +
|
||||||
|
((-0.5f * c0) + (1.5f * c1) - (1.5f * c2) + (0.5f * c3)) * 3.0f * phase2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/* 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
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
float catmull_rom_spline(float c0, float c1, float c2, float c3, float phase);
|
||||||
|
float catmull_rom_spline_gradient(float c0, float c1, float c2, float c3, float phase);
|
||||||
|
}
|
||||||
@@ -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 "math.hpp"
|
||||||
|
#include "muglm/muglm_impl.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
void quantize_color(uint8_t *v, const vec4 &color)
|
||||||
|
{
|
||||||
|
for (unsigned i = 0; i < 4; i++)
|
||||||
|
v[i] = uint8_t(muglm::round(muglm::clamp(color[i] * 255.0f, 0.0f, 255.0f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* 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 "muglm/muglm.hpp"
|
||||||
|
|
||||||
|
namespace Granite
|
||||||
|
{
|
||||||
|
using namespace muglm;
|
||||||
|
void quantize_color(uint8_t *v, const vec4 &color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/* 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 "muglm.hpp"
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace muglm
|
||||||
|
{
|
||||||
|
mat4 mat4_cast(const quat &q);
|
||||||
|
mat_affine mat_affine_cast(const quat &q);
|
||||||
|
mat3 mat3_cast(const quat &q);
|
||||||
|
mat4 translate(const vec3 &v);
|
||||||
|
mat4 scale(const vec3 &v);
|
||||||
|
mat_affine translate_affine(const vec3 &v);
|
||||||
|
mat_affine scale_affine(const vec3 &v);
|
||||||
|
mat2 inverse(const mat2 &m);
|
||||||
|
mat3 inverse(const mat3 &m);
|
||||||
|
mat4 inverse(const mat4 &m);
|
||||||
|
|
||||||
|
float determinant(const mat2 &m);
|
||||||
|
float determinant(const mat3 &m);
|
||||||
|
|
||||||
|
constexpr float InfiniteFarPlane = std::numeric_limits<float>::max();
|
||||||
|
mat4 perspective(float fovy, float aspect, float near, float far);
|
||||||
|
mat4 frustum(float left, float right, float bottom, float top, float near, float far);
|
||||||
|
|
||||||
|
// Orthogonal projection cannot have infinite far-plane.
|
||||||
|
mat4 ortho(float left, float right, float bottom, float top, float near, float far);
|
||||||
|
|
||||||
|
void decompose(const mat4 &m, vec3 &scale, quat &rot, vec3 &trans);
|
||||||
|
}
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
/* 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 "matrix_helper.hpp"
|
||||||
|
#include "muglm_impl.hpp"
|
||||||
|
#include "simd_headers.hpp"
|
||||||
|
|
||||||
|
namespace muglm
|
||||||
|
{
|
||||||
|
mat3 mat3_cast(const quat &q_)
|
||||||
|
{
|
||||||
|
auto &q = q_.as_vec4();
|
||||||
|
|
||||||
|
mat3 res(1.0f);
|
||||||
|
float qxx = q.x * q.x;
|
||||||
|
float qyy = q.y * q.y;
|
||||||
|
float qzz = q.z * q.z;
|
||||||
|
float qxz = q.x * q.z;
|
||||||
|
float qxy = q.x * q.y;
|
||||||
|
float qyz = q.y * q.z;
|
||||||
|
float qwx = q.w * q.x;
|
||||||
|
float qwy = q.w * q.y;
|
||||||
|
float qwz = q.w * q.z;
|
||||||
|
|
||||||
|
res[0][0] = 1.0f - 2.0f * (qyy + qzz);
|
||||||
|
res[0][1] = 2.0f * (qxy + qwz);
|
||||||
|
res[0][2] = 2.0f * (qxz - qwy);
|
||||||
|
|
||||||
|
res[1][0] = 2.0f * (qxy - qwz);
|
||||||
|
res[1][1] = 1.0f - 2.0f * (qxx + qzz);
|
||||||
|
res[1][2] = 2.0f * (qyz + qwx);
|
||||||
|
|
||||||
|
res[2][0] = 2.0f * (qxz + qwy);
|
||||||
|
res[2][1] = 2.0f * (qyz - qwx);
|
||||||
|
res[2][2] = 1.0f - 2.0f * (qxx + qyy);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 mat4_cast(const quat &q)
|
||||||
|
{
|
||||||
|
return mat4(mat3_cast(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
mat_affine mat_affine_cast(const quat &q)
|
||||||
|
{
|
||||||
|
return mat_affine(mat3_cast(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 translate(const vec3 &v)
|
||||||
|
{
|
||||||
|
return mat4(
|
||||||
|
vec4(1.0f, 0.0f, 0.0f, 0.0f),
|
||||||
|
vec4(0.0f, 1.0f, 0.0f, 0.0f),
|
||||||
|
vec4(0.0f, 0.0f, 1.0f, 0.0f),
|
||||||
|
vec4(v, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 scale(const vec3 &v)
|
||||||
|
{
|
||||||
|
return mat4(
|
||||||
|
vec4(v.x, 0.0f, 0.0f, 0.0f),
|
||||||
|
vec4(0.0f, v.y, 0.0f, 0.0f),
|
||||||
|
vec4(0.0f, 0.0f, v.z, 0.0f),
|
||||||
|
vec4(0.0f, 0.0f, 0.0f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
mat_affine translate_affine(const vec3 &v)
|
||||||
|
{
|
||||||
|
return mat_affine(
|
||||||
|
vec4(1.0f, 0.0f, 0.0f, v.x),
|
||||||
|
vec4(0.0f, 1.0f, 0.0f, v.y),
|
||||||
|
vec4(0.0f, 0.0f, 1.0f, v.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
mat_affine scale_affine(const vec3 &v)
|
||||||
|
{
|
||||||
|
return mat_affine(
|
||||||
|
vec4(v.x, 0.0f, 0.0f, 0.0f),
|
||||||
|
vec4(0.0f, v.y, 0.0f, 0.0f),
|
||||||
|
vec4(0.0f, 0.0f, v.z, 0.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
float determinant(const mat2 &m)
|
||||||
|
{
|
||||||
|
return m[0][0] * m[1][1] - m[1][0] * m[0][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
mat2 inverse(const mat2 &m)
|
||||||
|
{
|
||||||
|
float OneOverDeterminant = 1.0f / determinant(m);
|
||||||
|
|
||||||
|
mat2 Inverse(
|
||||||
|
vec2(m[1][1] * OneOverDeterminant,
|
||||||
|
-m[0][1] * OneOverDeterminant),
|
||||||
|
vec2(-m[1][0] * OneOverDeterminant,
|
||||||
|
m[0][0] * OneOverDeterminant));
|
||||||
|
|
||||||
|
return Inverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
float determinant(const mat3 &m)
|
||||||
|
{
|
||||||
|
return m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2])
|
||||||
|
- m[1][0] * (m[0][1] * m[2][2] - m[2][1] * m[0][2])
|
||||||
|
+ m[2][0] * (m[0][1] * m[1][2] - m[1][1] * m[0][2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat3 inverse(const mat3 &m)
|
||||||
|
{
|
||||||
|
float OneOverDeterminant = 1.0f / determinant(m);
|
||||||
|
|
||||||
|
mat3 Inverse;
|
||||||
|
Inverse[0][0] = +(m[1][1] * m[2][2] - m[2][1] * m[1][2]) * OneOverDeterminant;
|
||||||
|
Inverse[1][0] = -(m[1][0] * m[2][2] - m[2][0] * m[1][2]) * OneOverDeterminant;
|
||||||
|
Inverse[2][0] = +(m[1][0] * m[2][1] - m[2][0] * m[1][1]) * OneOverDeterminant;
|
||||||
|
Inverse[0][1] = -(m[0][1] * m[2][2] - m[2][1] * m[0][2]) * OneOverDeterminant;
|
||||||
|
Inverse[1][1] = +(m[0][0] * m[2][2] - m[2][0] * m[0][2]) * OneOverDeterminant;
|
||||||
|
Inverse[2][1] = -(m[0][0] * m[2][1] - m[2][0] * m[0][1]) * OneOverDeterminant;
|
||||||
|
Inverse[0][2] = +(m[0][1] * m[1][2] - m[1][1] * m[0][2]) * OneOverDeterminant;
|
||||||
|
Inverse[1][2] = -(m[0][0] * m[1][2] - m[1][0] * m[0][2]) * OneOverDeterminant;
|
||||||
|
Inverse[2][2] = +(m[0][0] * m[1][1] - m[1][0] * m[0][1]) * OneOverDeterminant;
|
||||||
|
|
||||||
|
return Inverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 inverse(const mat4 &m)
|
||||||
|
{
|
||||||
|
float Coef00 = m[2][2] * m[3][3] - m[3][2] * m[2][3];
|
||||||
|
float Coef02 = m[1][2] * m[3][3] - m[3][2] * m[1][3];
|
||||||
|
float Coef03 = m[1][2] * m[2][3] - m[2][2] * m[1][3];
|
||||||
|
|
||||||
|
float Coef04 = m[2][1] * m[3][3] - m[3][1] * m[2][3];
|
||||||
|
float Coef06 = m[1][1] * m[3][3] - m[3][1] * m[1][3];
|
||||||
|
float Coef07 = m[1][1] * m[2][3] - m[2][1] * m[1][3];
|
||||||
|
|
||||||
|
float Coef08 = m[2][1] * m[3][2] - m[3][1] * m[2][2];
|
||||||
|
float Coef10 = m[1][1] * m[3][2] - m[3][1] * m[1][2];
|
||||||
|
float Coef11 = m[1][1] * m[2][2] - m[2][1] * m[1][2];
|
||||||
|
|
||||||
|
float Coef12 = m[2][0] * m[3][3] - m[3][0] * m[2][3];
|
||||||
|
float Coef14 = m[1][0] * m[3][3] - m[3][0] * m[1][3];
|
||||||
|
float Coef15 = m[1][0] * m[2][3] - m[2][0] * m[1][3];
|
||||||
|
|
||||||
|
float Coef16 = m[2][0] * m[3][2] - m[3][0] * m[2][2];
|
||||||
|
float Coef18 = m[1][0] * m[3][2] - m[3][0] * m[1][2];
|
||||||
|
float Coef19 = m[1][0] * m[2][2] - m[2][0] * m[1][2];
|
||||||
|
|
||||||
|
float Coef20 = m[2][0] * m[3][1] - m[3][0] * m[2][1];
|
||||||
|
float Coef22 = m[1][0] * m[3][1] - m[3][0] * m[1][1];
|
||||||
|
float Coef23 = m[1][0] * m[2][1] - m[2][0] * m[1][1];
|
||||||
|
|
||||||
|
vec4 Fac0(Coef00, Coef00, Coef02, Coef03);
|
||||||
|
vec4 Fac1(Coef04, Coef04, Coef06, Coef07);
|
||||||
|
vec4 Fac2(Coef08, Coef08, Coef10, Coef11);
|
||||||
|
vec4 Fac3(Coef12, Coef12, Coef14, Coef15);
|
||||||
|
vec4 Fac4(Coef16, Coef16, Coef18, Coef19);
|
||||||
|
vec4 Fac5(Coef20, Coef20, Coef22, Coef23);
|
||||||
|
|
||||||
|
vec4 Vec0(m[1][0], m[0][0], m[0][0], m[0][0]);
|
||||||
|
vec4 Vec1(m[1][1], m[0][1], m[0][1], m[0][1]);
|
||||||
|
vec4 Vec2(m[1][2], m[0][2], m[0][2], m[0][2]);
|
||||||
|
vec4 Vec3(m[1][3], m[0][3], m[0][3], m[0][3]);
|
||||||
|
|
||||||
|
vec4 Inv0(Vec1 * Fac0 - Vec2 * Fac1 + Vec3 * Fac2);
|
||||||
|
vec4 Inv1(Vec0 * Fac0 - Vec2 * Fac3 + Vec3 * Fac4);
|
||||||
|
vec4 Inv2(Vec0 * Fac1 - Vec1 * Fac3 + Vec3 * Fac5);
|
||||||
|
vec4 Inv3(Vec0 * Fac2 - Vec1 * Fac4 + Vec2 * Fac5);
|
||||||
|
|
||||||
|
vec4 SignA(+1, -1, +1, -1);
|
||||||
|
vec4 SignB(-1, +1, -1, +1);
|
||||||
|
mat4 Inverse(Inv0 * SignA, Inv1 * SignB, Inv2 * SignA, Inv3 * SignB);
|
||||||
|
|
||||||
|
vec4 Row0(Inverse[0][0], Inverse[1][0], Inverse[2][0], Inverse[3][0]);
|
||||||
|
|
||||||
|
vec4 Dot0(m[0] * Row0);
|
||||||
|
float Dot1 = (Dot0.x + Dot0.y) + (Dot0.z + Dot0.w);
|
||||||
|
|
||||||
|
float OneOverDeterminant = 1.0f / Dot1;
|
||||||
|
|
||||||
|
return Inverse * OneOverDeterminant;
|
||||||
|
}
|
||||||
|
|
||||||
|
void decompose(const mat4 &m, vec3 &scale, quat &rotation, vec3 &trans)
|
||||||
|
{
|
||||||
|
vec4 rot;
|
||||||
|
|
||||||
|
// Make a lot of assumptions.
|
||||||
|
// We don't need skew, nor perspective.
|
||||||
|
|
||||||
|
// Isolate translation.
|
||||||
|
trans = m[3].xyz();
|
||||||
|
|
||||||
|
vec3 cols[3];
|
||||||
|
cols[0] = m[0].xyz();
|
||||||
|
cols[1] = m[1].xyz();
|
||||||
|
cols[2] = m[2].xyz();
|
||||||
|
|
||||||
|
scale.x = length(cols[0]);
|
||||||
|
scale.y = length(cols[1]);
|
||||||
|
scale.z = length(cols[2]);
|
||||||
|
|
||||||
|
// Isolate scale.
|
||||||
|
cols[0] /= scale.x;
|
||||||
|
cols[1] /= scale.y;
|
||||||
|
cols[2] /= scale.z;
|
||||||
|
|
||||||
|
vec3 pdum3 = cross(cols[1], cols[2]);
|
||||||
|
if (dot(cols[0], pdum3) < 0.0f)
|
||||||
|
{
|
||||||
|
scale = -scale;
|
||||||
|
cols[0] = -cols[0];
|
||||||
|
cols[1] = -cols[1];
|
||||||
|
cols[2] = -cols[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
int i, j, k = 0;
|
||||||
|
float root, trace = cols[0].x + cols[1].y + cols[2].z;
|
||||||
|
if (trace > 0.0f)
|
||||||
|
{
|
||||||
|
root = sqrt(trace + 1.0f);
|
||||||
|
rot.w = 0.5f * root;
|
||||||
|
root = 0.5f / root;
|
||||||
|
rot.x = root * (cols[1].z - cols[2].y);
|
||||||
|
rot.y = root * (cols[2].x - cols[0].z);
|
||||||
|
rot.z = root * (cols[0].y - cols[1].x);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
static const int Next[3] = {1, 2, 0};
|
||||||
|
|
||||||
|
i = 0;
|
||||||
|
if (cols[1].y > cols[0].x) i = 1;
|
||||||
|
if (cols[2].z > cols[i][i]) i = 2;
|
||||||
|
|
||||||
|
j = Next[i];
|
||||||
|
k = Next[j];
|
||||||
|
|
||||||
|
root = sqrt(cols[i][i] - cols[j][j] - cols[k][k] + 1.0f);
|
||||||
|
|
||||||
|
rot[i] = 0.5f * root;
|
||||||
|
root = 0.5f / root;
|
||||||
|
rot[j] = root * (cols[i][j] + cols[j][i]);
|
||||||
|
rot[k] = root * (cols[i][k] + cols[k][i]);
|
||||||
|
rot.w = root * (cols[j][k] - cols[k][j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rotation = quat(rot);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 ortho(float left, float right, float bottom, float top, float near, float far)
|
||||||
|
{
|
||||||
|
mat4 result(1.0f);
|
||||||
|
result[0][0] = 2.0f / (right - left);
|
||||||
|
result[1][1] = 2.0f / (top - bottom);
|
||||||
|
result[3][0] = -(right + left) / (right - left);
|
||||||
|
result[3][1] = -(top + bottom) / (top - bottom);
|
||||||
|
|
||||||
|
result[2][2] = 1.0f / (far - near);
|
||||||
|
result[3][2] = 1.0f + near / (far - near);
|
||||||
|
|
||||||
|
result[0].y *= -1.0f;
|
||||||
|
result[1].y *= -1.0f;
|
||||||
|
result[2].y *= -1.0f;
|
||||||
|
result[3].y *= -1.0f;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 frustum(float left, float right, float bottom, float top, float near, float far)
|
||||||
|
{
|
||||||
|
mat4 result(0.0f);
|
||||||
|
result[0][0] = (2.0f * near) / (right - left);
|
||||||
|
result[1][1] = (2.0f * near) / (top - bottom);
|
||||||
|
result[2][0] = (right + left) / (right - left);
|
||||||
|
result[2][1] = (top + bottom) / (top - bottom);
|
||||||
|
|
||||||
|
// Inverse Z
|
||||||
|
if (far == InfiniteFarPlane)
|
||||||
|
{
|
||||||
|
result[3][2] = -near;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result[2][2] = -1.0f - far / (near - far);
|
||||||
|
result[3][2] = -(far * near) / (near - far);
|
||||||
|
}
|
||||||
|
|
||||||
|
result[2][3] = -1.0f;
|
||||||
|
|
||||||
|
// Y-flip so we don't have to bother with negative viewport heights.
|
||||||
|
result[0].y *= -1.0f;
|
||||||
|
result[1].y *= -1.0f;
|
||||||
|
result[2].y *= -1.0f;
|
||||||
|
result[3].y *= -1.0f;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 perspective(float fovy, float aspect, float near, float far)
|
||||||
|
{
|
||||||
|
float tanHalfFovy = tan(fovy / 2.0f);
|
||||||
|
|
||||||
|
mat4 result(0.0f);
|
||||||
|
result[0][0] = 1.0f / (aspect * tanHalfFovy);
|
||||||
|
result[1][1] = 1.0f / (tanHalfFovy);
|
||||||
|
|
||||||
|
// Inverse Z
|
||||||
|
if (far == InfiniteFarPlane)
|
||||||
|
{
|
||||||
|
result[3][2] = near;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result[2][2] = -1.0f - far / (near - far);
|
||||||
|
result[3][2] = -(far * near) / (near - far);
|
||||||
|
}
|
||||||
|
|
||||||
|
result[2][3] = -1.0f;
|
||||||
|
|
||||||
|
// Y-flip so we don't have to bother with negative viewport heights.
|
||||||
|
result[0].y *= -1.0f;
|
||||||
|
result[1].y *= -1.0f;
|
||||||
|
result[2].y *= -1.0f;
|
||||||
|
result[3].y *= -1.0f;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void transpose(mat4 &dst, const mat4 &src)
|
||||||
|
{
|
||||||
|
#if __SSE__
|
||||||
|
__m128 r0 = _mm_loadu_ps(src[0].data);
|
||||||
|
__m128 r1 = _mm_loadu_ps(src[1].data);
|
||||||
|
__m128 r2 = _mm_loadu_ps(src[2].data);
|
||||||
|
__m128 r3 = _mm_loadu_ps(src[3].data);
|
||||||
|
_MM_TRANSPOSE4_PS(r0, r1, r2, r3);
|
||||||
|
_mm_storeu_ps(dst[0].data, r0);
|
||||||
|
_mm_storeu_ps(dst[1].data, r1);
|
||||||
|
_mm_storeu_ps(dst[2].data, r2);
|
||||||
|
_mm_storeu_ps(dst[3].data, r3);
|
||||||
|
#elif defined(__ARM_NEON)
|
||||||
|
float32x4x4_t a = vld4q_f32(src[0].data);
|
||||||
|
vst1q_f32(dst[0].data, a.val[0]);
|
||||||
|
vst1q_f32(dst[1].data, a.val[1]);
|
||||||
|
vst1q_f32(dst[2].data, a.val[2]);
|
||||||
|
vst1q_f32(dst[3].data, a.val[3]);
|
||||||
|
#else
|
||||||
|
dst = transpose(src);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void transpose_to_affine(vec4 dst[3], const mat4 &src)
|
||||||
|
{
|
||||||
|
#if __SSE__
|
||||||
|
__m128 r0 = _mm_loadu_ps(src[0].data);
|
||||||
|
__m128 r1 = _mm_loadu_ps(src[1].data);
|
||||||
|
__m128 r2 = _mm_loadu_ps(src[2].data);
|
||||||
|
__m128 r3 = _mm_loadu_ps(src[3].data);
|
||||||
|
_MM_TRANSPOSE4_PS(r0, r1, r2, r3);
|
||||||
|
_mm_storeu_ps(dst[0].data, r0);
|
||||||
|
_mm_storeu_ps(dst[1].data, r1);
|
||||||
|
_mm_storeu_ps(dst[2].data, r2);
|
||||||
|
#elif defined(__ARM_NEON)
|
||||||
|
float32x4x4_t a = vld4q_f32(src[0].data);
|
||||||
|
vst1q_f32(dst[0].data, a.val[0]);
|
||||||
|
vst1q_f32(dst[1].data, a.val[1]);
|
||||||
|
vst1q_f32(dst[2].data, a.val[2]);
|
||||||
|
#else
|
||||||
|
mat4 m = transpose(src);
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
dst[i] = m[i];
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void transpose_from_affine(mat4 &dst, const vec4 src[3])
|
||||||
|
{
|
||||||
|
#if __SSE__
|
||||||
|
__m128 r0 = _mm_loadu_ps(src[0].data);
|
||||||
|
__m128 r1 = _mm_loadu_ps(src[1].data);
|
||||||
|
__m128 r2 = _mm_loadu_ps(src[2].data);
|
||||||
|
__m128 r3 = _mm_set_ps(1, 0, 0, 0);
|
||||||
|
_MM_TRANSPOSE4_PS(r0, r1, r2, r3);
|
||||||
|
_mm_storeu_ps(dst[0].data, r0);
|
||||||
|
_mm_storeu_ps(dst[1].data, r1);
|
||||||
|
_mm_storeu_ps(dst[2].data, r2);
|
||||||
|
_mm_storeu_ps(dst[3].data, r3);
|
||||||
|
#elif defined(__ARM_NEON)
|
||||||
|
alignas(16) static const float r3_data[] = { 0, 0, 0, 1 };
|
||||||
|
float32x4_t r0 = vld1q_f32(src[0].data);
|
||||||
|
float32x4_t r1 = vld1q_f32(src[1].data);
|
||||||
|
float32x4_t r2 = vld1q_f32(src[2].data);
|
||||||
|
float32x4_t r3 = vld1q_f32(r3_data);
|
||||||
|
float32x4x4_t r = { r0, r1, r2, r3 };
|
||||||
|
vst4q_f32(dst[0].data, r);
|
||||||
|
#else
|
||||||
|
mat4 m = transpose(src);
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
dst[i] = m[i];
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void mat_affine::to_mat4(muglm::mat4 &m) const
|
||||||
|
{
|
||||||
|
transpose_from_affine(m, vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 mat_affine::to_mat4() const
|
||||||
|
{
|
||||||
|
mat4 m;
|
||||||
|
to_mat4(m);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
float mat_affine::get_uniform_scale() const
|
||||||
|
{
|
||||||
|
return length(vec[0].xyz());
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 mat_affine::get_translation() const
|
||||||
|
{
|
||||||
|
// this * vec4(0, 0, 0, 1)
|
||||||
|
return { vec[0].w, vec[1].w, vec[2].w };
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 mat_affine::get_forward() const
|
||||||
|
{
|
||||||
|
// this * vec4(0, 0, -1, 0).
|
||||||
|
return { -vec[0].z, -vec[1].z, -vec[2].z };
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 mat_affine::get_right() const
|
||||||
|
{
|
||||||
|
return { vec[0].x, vec[1].x, vec[2].x };
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 mat_affine::get_up() const
|
||||||
|
{
|
||||||
|
return { vec[0].y, vec[1].y, vec[2].y };
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user