feat(host): vendor PyroWave + minimal Granite subset as crates/pyrowave-sys

Phase 0 of design/pyrowave-codec-plan.md — the opt-in wired-LAN ultra-low-
latency codec. Vendored at upstream 509e4f88 (API 0.4.0, Granite 44362775,
volk + vulkan-headers pins in PUNKTFUNK-VENDOR.txt), pruned to the 6.6 MB
the standalone no-renderer build needs; scripts/vendor-pyrowave.sh
reproduces the tree (a pin bump is protocol-affecting, plan §4.2).

build.rs drives the wrapper CMakeLists (static archives incl. a static
C-API lib upstream only ships shared) + bindgen over pyrowave.h; Linux and
Windows only, empty stub elsewhere (Apple gets a native Metal port, §4.7).
Offline-safe by construction: no network, no system lib, vendored Vulkan
headers — same model as the opus dep (flatpak builder has no network).

Phase-0 validation on .21 (RTX 5070 Ti, driver 610.43.03):
- upstream pyrowave-c-test + interop test (incl. dmabuf/DRM-modifier
  Vulkan<->Vulkan) pass, from the pristine AND the pruned tree
- GPU kernel times at ~1.6 bpp noise: encode/decode 0.090/0.042 ms @800p,
  0.146/0.067 @1080p, 0.226/0.103 @1440p, 0.477/0.201 @4K — order of
  magnitude under NVENC's 1-2 ms retrieve, CBR lands within ~100 B of
  target
- cargo test -p pyrowave-sys green (static link + API-version pin check)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:35:10 +02:00
parent 1b73361372
commit 4c3b11445c
396 changed files with 140058 additions and 0 deletions
Generated
+8
View File
@@ -3190,6 +3190,14 @@ dependencies = [
"winresource",
]
[[package]]
name = "pyrowave-sys"
version = "0.11.0"
dependencies = [
"bindgen",
"cmake",
]
[[package]]
name = "quick-error"
version = "1.2.3"
+1
View File
@@ -10,6 +10,7 @@ members = [
"crates/pf-console-ui",
"crates/pf-ffvk",
"crates/pf-driver-proto",
"crates/pyrowave-sys",
"clients/probe",
"clients/linux",
"clients/session",
+30
View File
@@ -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)
+17
View File
@@ -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 }
+103
View File
@@ -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");
}
+29
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+20
View File
@@ -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.
+632
View File
@@ -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.
+205
View File
@@ -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();
};
}
@@ -0,0 +1,100 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "application.hpp"
#include "filesystem.hpp"
#include "path_utils.hpp"
//#define USE_FP_EXCEPTIONS
#ifdef USE_FP_EXCEPTIONS
#include <fenv.h>
#endif
#if defined(_WIN32) && !defined(APPLICATION_ENTRY_HEADLESS)
#define USE_WINMAIN
#endif
#ifdef USE_WINMAIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <vector>
#endif
namespace Granite
{
// Make sure this is linked in.
void application_dummy()
{
}
// Alternatively, make sure this is linked in.
// Implementation is here to trick a linker to always let main() in static library work.
void application_setup_default_filesystem(const char *default_asset_directory)
{
auto *filesystem = GRANITE_FILESYSTEM();
if (filesystem)
Filesystem::setup_default_filesystem(filesystem, default_asset_directory);
}
}
#ifdef USE_WINMAIN
int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
#else
int main(int argc, char *argv[])
#endif
{
#ifdef USE_FP_EXCEPTIONS
feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW);
#endif
#ifdef USE_WINMAIN
int argc;
wchar_t **wide_argv = CommandLineToArgvW(GetCommandLineW(), &argc);
std::vector<char *> argv_buffer(argc + 1);
char **argv = nullptr;
std::vector<std::string> argv_strings(argc);
if (wide_argv)
{
argv = argv_buffer.data();
for (int i = 0; i < argc; i++)
{
argv_strings[i] = Granite::Path::to_utf8(wide_argv[i]);
argv_buffer[i] = const_cast<char *>(argv_strings[i].c_str());
}
}
#endif
#ifdef APPLICATION_ENTRY_HEADLESS
int ret = Granite::application_main_headless(Granite::query_application_interface,
Granite::application_create,
argc, argv);
#else
int ret = Granite::application_main(Granite::query_application_interface,
Granite::application_create,
argc, argv);
#endif
return ret;
}
@@ -0,0 +1,68 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace Granite
{
enum class ApplicationQuery
{
DefaultManagerFlags
};
class Application;
int application_main(
bool (*query_application_interface)(ApplicationQuery, void *data, size_t size),
Application *(*create_application)(int, char **),
int argc, char **argv);
int application_main_headless(
bool (*query_application_interface)(ApplicationQuery, void *data, size_t size),
Application *(*create_application)(int, char **),
int argc, char **argv);
extern Application *application_create(int argc, char *argv[]);
struct ApplicationQueryDefaultManagerFlags
{
uint32_t manager_feature_flags;
};
extern bool query_application_interface(ApplicationQuery query, void *data, size_t size);
// Call this or setup_default_filesystem to ensure application-main is linked in correctly without having to mess around
// with -Wl,--whole-archive.
void application_dummy();
void application_setup_default_filesystem(const char *default_asset_directory);
}
#ifdef ASSET_DIRECTORY
#define GRANITE_APPLICATION_SETUP_FILESYSTEM() ::Granite::application_setup_default_filesystem(ASSET_DIRECTORY)
#else
#define GRANITE_APPLICATION_SETUP_FILESYSTEM() ::Granite::application_setup_default_filesystem(nullptr)
#endif
#define GRANITE_APPLICATION_DECL_DEFAULT_QUERY() namespace Granite { bool query_application_interface(Granite::ApplicationQuery, void *, size_t) { return false; } }
@@ -0,0 +1,2 @@
#include "application_glue.hpp"
GRANITE_APPLICATION_DECL_DEFAULT_QUERY()
@@ -0,0 +1,7 @@
add_granite_internal_lib(granite-application-events
application_wsi.hpp
application_wsi.cpp application_wsi_events.hpp
application_events.hpp)
target_include_directories(granite-application-events PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-events
PRIVATE granite-vulkan granite-event granite-input granite-application-global)
@@ -0,0 +1,55 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "event.hpp"
namespace Granite
{
enum class ApplicationLifecycle
{
Running,
Paused,
Stopped,
Dead
};
class ApplicationLifecycleEvent : public Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationLifecycleEvent)
explicit ApplicationLifecycleEvent(ApplicationLifecycle lifecycle_)
: lifecycle(lifecycle_)
{
}
ApplicationLifecycle get_lifecycle() const
{
return lifecycle;
}
private:
ApplicationLifecycle lifecycle;
};
}
@@ -0,0 +1,188 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "application_wsi.hpp"
#include "application_wsi_events.hpp"
#include "application_events.hpp"
#include "global_managers.hpp"
#include "event.hpp"
namespace Granite
{
GraniteWSIPlatform::GraniteWSIPlatform()
{
input_tracker.set_input_handler(this);
}
void GraniteWSIPlatform::event_swapchain_created(Vulkan::Device *device, VkSwapchainKHR, unsigned width, unsigned height,
float aspect_ratio, size_t image_count, VkFormat format,
VkColorSpaceKHR color_space,
VkSurfaceTransformFlagBitsKHR transform)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->enqueue_latched<Vulkan::SwapchainParameterEvent>(device, width, height, aspect_ratio, image_count, format,
color_space, transform);
}
}
void GraniteWSIPlatform::event_swapchain_destroyed()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->dequeue_all_latched(Vulkan::SwapchainParameterEvent::get_type_id());
}
void GraniteWSIPlatform::event_device_created(Vulkan::Device *device)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->enqueue_latched<Vulkan::DeviceCreatedEvent>(device);
}
void GraniteWSIPlatform::event_device_destroyed()
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
em->dequeue_all_latched(Vulkan::DeviceCreatedEvent::get_type_id());
}
void GraniteWSIPlatform::event_swapchain_index(Vulkan::Device *device, unsigned index)
{
auto *em = GRANITE_EVENT_MANAGER();
if (em)
{
em->dequeue_all_latched(Vulkan::SwapchainIndexEvent::get_type_id());
em->enqueue_latched<Vulkan::SwapchainIndexEvent>(device, index);
}
}
void GraniteWSIPlatform::event_frame_tick(double, double)
{
}
template <typename T>
void GraniteWSIPlatform::dispatch_template(const T &t)
{
if (auto *em = GRANITE_EVENT_MANAGER())
em->dispatch_inline(t);
}
template <typename T>
void GraniteWSIPlatform::dispatch_template_filter(const T &t)
{
auto *ui = Global::ui_manager();
if (ui && !ui->filter_input_event(t))
return;
dispatch_template(t);
}
template <typename Func>
void GraniteWSIPlatform::dispatch_or_defer(Func &&func)
{
if (in_async_input)
captured.emplace_back(std::forward<Func>(func));
else
func();
}
#define WORK(work) dispatch_or_defer([this, e]() { work; })
void GraniteWSIPlatform::dispatch(const TouchDownEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const TouchUpEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const TouchGestureEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadButtonEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadAxisEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const KeyboardEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const OrientationEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const MouseButtonEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const MouseMoveEvent &e)
{
WORK(dispatch_template_filter(e));
}
void GraniteWSIPlatform::dispatch(const JoypadStateEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::dispatch(const InputStateEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::dispatch(const JoypadConnectionEvent &e)
{
WORK(dispatch_template(e));
}
void GraniteWSIPlatform::begin_async_input_handling()
{
in_async_input = true;
}
void GraniteWSIPlatform::end_async_input_handling()
{
in_async_input = false;
}
void GraniteWSIPlatform::flush_deferred_input_events()
{
VK_ASSERT(!in_async_input);
for (auto &func : captured)
func();
captured.clear();
}
}
@@ -0,0 +1,81 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "wsi.hpp"
#include "input.hpp"
#include <functional>
namespace Granite
{
class GraniteWSIPlatform : public Vulkan::WSIPlatform, private InputTrackerHandler
{
public:
GraniteWSIPlatform();
InputTracker &get_input_tracker()
{
return input_tracker;
}
protected:
void event_device_created(Vulkan::Device *device) override;
void event_device_destroyed() override;
void event_swapchain_created(Vulkan::Device *device, VkSwapchainKHR swapchain,
unsigned width, unsigned height,
float aspect_ratio, size_t image_count,
VkFormat format, VkColorSpaceKHR color_space,
VkSurfaceTransformFlagBitsKHR pre_rotate) override;
void event_swapchain_destroyed() override;
void event_swapchain_index(Vulkan::Device *device, unsigned index) override;
void event_frame_tick(double frame, double elapsed) override;
void begin_async_input_handling();
void end_async_input_handling();
void flush_deferred_input_events();
private:
InputTracker input_tracker;
void dispatch(const TouchDownEvent &e) override;
void dispatch(const TouchUpEvent &e) override;
void dispatch(const TouchGestureEvent &e) override;
void dispatch(const JoypadButtonEvent &e) override;
void dispatch(const JoypadAxisEvent &e) override;
void dispatch(const KeyboardEvent &e) override;
void dispatch(const OrientationEvent &e) override;
void dispatch(const MouseButtonEvent &e) override;
void dispatch(const MouseMoveEvent &e) override;
void dispatch(const JoypadStateEvent &e) override;
void dispatch(const InputStateEvent &e) override;
void dispatch(const JoypadConnectionEvent &e) override;
template <typename T>
void dispatch_template_filter(const T &t);
template <typename T>
void dispatch_template(const T &t);
template <typename Func>
void dispatch_or_defer(Func &&func);
bool in_async_input = false;
std::vector<std::function<void ()>> captured;
};
}
@@ -0,0 +1,258 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "event.hpp"
#include "vulkan_headers.hpp"
namespace Vulkan
{
class Device;
class ShaderManager;
class WSIPlatform;
class DeviceCreatedEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DeviceCreatedEvent)
explicit DeviceCreatedEvent(Device *device_)
: device(*device_)
{}
Device &get_device() const
{
return device;
}
private:
Device &device;
};
class DeviceShaderModuleReadyEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DeviceShaderModuleReadyEvent)
explicit DeviceShaderModuleReadyEvent(Device *device_, ShaderManager *manager_)
: device(*device_), manager(*manager_)
{}
Device &get_device() const
{
return device;
}
ShaderManager &get_shader_manager() const
{
return manager;
}
private:
Device &device;
ShaderManager &manager;
};
class DevicePipelineReadyEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(DevicePipelineReadyEvent)
explicit DevicePipelineReadyEvent(Device *device_, ShaderManager *manager_)
: device(*device_), manager(*manager_)
{}
Device &get_device() const
{
return device;
}
ShaderManager &get_shader_manager() const
{
return manager;
}
private:
Device &device;
ShaderManager &manager;
};
class SwapchainParameterEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(SwapchainParameterEvent)
SwapchainParameterEvent(Device *device_,
unsigned width_, unsigned height_,
float aspect_ratio_, unsigned count_,
VkFormat format_, VkColorSpaceKHR color_space_,
VkSurfaceTransformFlagBitsKHR transform_)
: device(*device_), width(width_), height(height_),
aspect_ratio(aspect_ratio_), image_count(count_), format(format_), color_space(color_space_), transform(transform_)
{}
Device &get_device() const
{
return device;
}
unsigned get_width() const
{
return width;
}
unsigned get_height() const
{
return height;
}
float get_aspect_ratio() const
{
return aspect_ratio;
}
unsigned get_image_count() const
{
return image_count;
}
VkFormat get_format() const
{
return format;
}
VkColorSpaceKHR get_color_space() const
{
return color_space;
}
VkSurfaceTransformFlagBitsKHR get_prerotate() const
{
return transform;
}
private:
Device &device;
unsigned width;
unsigned height;
float aspect_ratio;
unsigned image_count;
VkFormat format;
VkColorSpaceKHR color_space;
VkSurfaceTransformFlagBitsKHR transform;
};
class SwapchainIndexEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(SwapchainIndexEvent)
SwapchainIndexEvent(Device *device_, unsigned index_)
: device(*device_), index(index_)
{}
Device &get_device() const
{
return device;
}
unsigned get_index() const
{
return index;
}
private:
Device &device;
unsigned index;
};
class ApplicationWSIPlatformEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWSIPlatformEvent)
explicit ApplicationWSIPlatformEvent(WSIPlatform &platform_)
: platform(platform_)
{}
WSIPlatform &get_platform() const
{
return platform;
}
private:
WSIPlatform &platform;
};
class ApplicationWindowFileDropEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWindowFileDropEvent)
explicit ApplicationWindowFileDropEvent(std::string path_)
: path(std::move(path_))
{}
const std::string &get_path() const
{
return path;
}
private:
std::string path;
};
class ApplicationWindowTextDropEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationWindowTextDropEvent)
explicit ApplicationWindowTextDropEvent(std::string str_)
: str(std::move(str_))
{}
const std::string &get_text() const
{
return str;
}
private:
std::string str;
};
class ApplicationSoftKeyboardUpdateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(ApplicationSoftKeyboardUpdateEvent)
explicit ApplicationSoftKeyboardUpdateEvent(std::string str_)
: str(std::move(str_))
{
}
const std::string &get_text() const
{
return str;
}
private:
std::string str;
};
}
@@ -0,0 +1,27 @@
add_granite_internal_lib(granite-application-global
global_managers.hpp global_managers.cpp)
target_include_directories(granite-application-global PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-global PUBLIC granite-util)
add_granite_internal_lib(granite-application-global-init
global_managers_init.hpp global_managers_init.cpp)
target_include_directories(granite-application-global-init PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-application-global-init
PUBLIC granite-application-global
PRIVATE granite-threading granite-event granite-filesystem)
if (TARGET granite-renderer)
target_link_libraries(granite-application-global-init PRIVATE granite-renderer granite-ui)
target_compile_definitions(granite-application-global-init PRIVATE HAVE_GRANITE_RENDERER)
endif()
if (GRANITE_AUDIO)
target_link_libraries(granite-application-global-init PRIVATE granite-audio)
endif()
if (GRANITE_BULLET)
target_link_libraries(granite-application-global-init PRIVATE granite-physics)
endif()
add_library(granite-application-global-interface INTERFACE)
target_include_directories(granite-application-global-interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
@@ -0,0 +1,308 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "global_managers.hpp"
#include "environment.hpp"
#include "logging.hpp"
#include <thread>
#include <assert.h>
#include <stdlib.h>
namespace Granite
{
namespace Global
{
// Could use unique_ptr here, but would be nice to avoid global ctor/dtor.
struct GlobalManagers
{
Factory *factory;
FilesystemInterface *filesystem;
AssetManagerInterface *asset_manager;
MaterialManagerInterface *material_manager;
EventManagerInterface *event_manager;
ThreadGroupInterface *thread_group;
UI::UIManagerInterface *ui_manager;
CommonRendererDataInterface *common_renderer_data;
Util::MessageQueueInterface *logging;
Audio::BackendInterface *audio_backend;
Audio::MixerInterface *audio_mixer;
PhysicsSystemInterface *physics;
};
static thread_local GlobalManagers global_managers;
GlobalManagersHandle create_thread_context()
{
return GlobalManagersHandle(new GlobalManagers(global_managers));
}
void delete_thread_context(GlobalManagers *managers)
{
delete managers;
}
void GlobalManagerDeleter::operator()(GlobalManagers *managers)
{
delete_thread_context(managers);
}
void set_thread_context(const GlobalManagers &managers)
{
global_managers = managers;
if (managers.thread_group)
managers.thread_group->set_thread_context();
if (managers.logging)
Util::set_thread_logging_interface(managers.logging);
}
void clear_thread_context()
{
global_managers = {};
}
Util::MessageQueueInterface *message_queue()
{
return global_managers.logging;
}
FilesystemInterface *filesystem()
{
return global_managers.filesystem;
}
AssetManagerInterface *asset_manager()
{
return global_managers.asset_manager;
}
MaterialManagerInterface *material_manager()
{
return global_managers.material_manager;
}
EventManagerInterface *event_manager()
{
return global_managers.event_manager;
}
ThreadGroupInterface *thread_group()
{
return global_managers.thread_group;
}
UI::UIManagerInterface *ui_manager()
{
return global_managers.ui_manager;
}
CommonRendererDataInterface *common_renderer_data()
{
return global_managers.common_renderer_data;
}
Audio::BackendInterface *audio_backend() { return global_managers.audio_backend; }
Audio::MixerInterface *audio_mixer() { return global_managers.audio_mixer; }
void install_audio_system(Audio::BackendInterface *backend, Audio::MixerInterface *mixer)
{
delete global_managers.audio_mixer;
global_managers.audio_mixer = mixer;
delete global_managers.audio_backend;
global_managers.audio_backend = backend;
}
PhysicsSystemInterface *physics()
{
return global_managers.physics;
}
void init(Factory &factory, ManagerFeatureFlags flags, unsigned max_threads, float audio_sample_rate)
{
assert(!global_managers.factory || global_managers.factory == &factory);
global_managers.factory = &factory;
if (flags & MANAGER_FEATURE_EVENT_BIT)
{
if (!global_managers.event_manager)
global_managers.event_manager = factory.create_event_manager();
}
if (flags & MANAGER_FEATURE_FILESYSTEM_BIT)
{
if (!global_managers.filesystem)
global_managers.filesystem = factory.create_filesystem();
}
if (flags & MANAGER_FEATURE_ASSET_MANAGER_BIT)
{
if (!global_managers.asset_manager)
global_managers.asset_manager = factory.create_asset_manager();
}
if (flags & MANAGER_FEATURE_MATERIAL_MANAGER_BIT)
{
if (!global_managers.material_manager)
global_managers.material_manager = factory.create_material_manager();
}
bool kick_threads = false;
if (flags & MANAGER_FEATURE_THREAD_GROUP_BIT)
{
if (!global_managers.thread_group)
{
global_managers.thread_group = factory.create_thread_group();
kick_threads = true;
}
}
if (flags & MANAGER_FEATURE_UI_MANAGER_BIT)
{
if (!global_managers.ui_manager)
global_managers.ui_manager = factory.create_ui_manager();
}
if (flags & MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT)
{
if (!global_managers.common_renderer_data)
global_managers.common_renderer_data = factory.create_common_renderer_data();
}
if (flags & MANAGER_FEATURE_LOGGING_BIT)
{
if (!global_managers.logging)
global_managers.logging = factory.create_message_queue();
Util::set_thread_logging_interface(global_managers.logging);
}
if (flags & MANAGER_FEATURE_PHYSICS_BIT)
{
if (!global_managers.physics)
global_managers.physics = factory.create_physics_system();
}
if (flags & MANAGER_FEATURE_AUDIO_MIXER_BIT)
{
if (!global_managers.audio_mixer)
global_managers.audio_mixer = factory.create_audio_mixer();
}
if (flags & MANAGER_FEATURE_AUDIO_BACKEND_BIT)
{
if (!global_managers.audio_backend)
global_managers.audio_backend = factory.create_audio_backend(global_managers.audio_mixer, audio_sample_rate, 2);
}
// Kick threads after all global managers are set up.
if (kick_threads)
{
unsigned cpu_threads = std::thread::hardware_concurrency();
cpu_threads = cpu_threads > 1 ? (cpu_threads - 1u) : 1u;
if (cpu_threads > max_threads)
cpu_threads = max_threads;
cpu_threads = Util::get_environment_uint("GRANITE_NUM_WORKER_THREADS", cpu_threads);
unsigned background_cpu_threads = (cpu_threads + 1) / 2;
global_managers.thread_group->start(cpu_threads, background_cpu_threads,
[ctx = std::shared_ptr<GlobalManagers>(create_thread_context())] {
set_thread_context(*ctx);
});
}
}
void deinit()
{
if (!global_managers.factory)
return;
if (global_managers.audio_backend)
global_managers.audio_backend->stop();
delete global_managers.audio_backend;
delete global_managers.audio_mixer;
delete global_managers.physics;
delete global_managers.common_renderer_data;
delete global_managers.ui_manager;
delete global_managers.thread_group;
delete global_managers.material_manager;
delete global_managers.asset_manager;
delete global_managers.filesystem;
delete global_managers.event_manager;
delete global_managers.logging;
global_managers.audio_backend = nullptr;
global_managers.audio_mixer = nullptr;
global_managers.physics = nullptr;
global_managers.common_renderer_data = nullptr;
global_managers.filesystem = nullptr;
global_managers.material_manager = nullptr;
global_managers.asset_manager = nullptr;
global_managers.event_manager = nullptr;
global_managers.thread_group = nullptr;
global_managers.ui_manager = nullptr;
global_managers.logging = nullptr;
global_managers.factory = nullptr;
}
void start_audio_system()
{
if (!global_managers.audio_backend)
return;
if (!global_managers.audio_backend->start())
{
LOGE("Failed to start audio subsystem!\n");
return;
}
if (global_managers.event_manager && global_managers.audio_mixer)
global_managers.audio_mixer->event_start(*global_managers.event_manager);
}
void stop_audio_system()
{
if (!global_managers.audio_backend)
return;
if (!global_managers.audio_backend->stop())
LOGE("Failed to stop audio subsystem!\n");
if (global_managers.event_manager && global_managers.audio_mixer)
global_managers.audio_mixer->event_stop(*global_managers.event_manager);
}
FilesystemInterface *Factory::create_filesystem() { return nullptr; }
AssetManagerInterface *Factory::create_asset_manager() { return nullptr; }
MaterialManagerInterface *Factory::create_material_manager() { return nullptr; }
EventManagerInterface *Factory::create_event_manager() { return nullptr; }
ThreadGroupInterface *Factory::create_thread_group() { return nullptr; }
CommonRendererDataInterface *Factory::create_common_renderer_data() { return nullptr; }
PhysicsSystemInterface *Factory::create_physics_system() { return nullptr; }
Audio::BackendInterface *Factory::create_audio_backend(Audio::MixerInterface *, float, unsigned) { return nullptr; }
Audio::MixerInterface *Factory::create_audio_mixer() { return nullptr; }
UI::UIManagerInterface *Factory::create_ui_manager() { return nullptr; }
Util::MessageQueueInterface *Factory::create_message_queue() { return nullptr; }
}
}
@@ -0,0 +1,127 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include <functional>
#include <memory>
#include <limits.h>
#include "global_managers_interface.hpp"
namespace Granite
{
namespace Global
{
enum ManagerFeatureFlagBits
{
MANAGER_FEATURE_FILESYSTEM_BIT = 1 << 0,
MANAGER_FEATURE_EVENT_BIT = 1 << 1,
MANAGER_FEATURE_THREAD_GROUP_BIT = 1 << 2,
MANAGER_FEATURE_UI_MANAGER_BIT = 1 << 3,
MANAGER_FEATURE_AUDIO_MIXER_BIT = 1 << 4,
MANAGER_FEATURE_AUDIO_BACKEND_BIT = 1 << 5,
MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT = 1 << 6,
MANAGER_FEATURE_PHYSICS_BIT = 1 << 7,
MANAGER_FEATURE_LOGGING_BIT = 1 << 8,
MANAGER_FEATURE_ASSET_MANAGER_BIT = 1 << 9,
MANAGER_FEATURE_MATERIAL_MANAGER_BIT = 1 << 10,
MANAGER_FEATURE_DEFAULT_BITS = (MANAGER_FEATURE_FILESYSTEM_BIT |
MANAGER_FEATURE_ASSET_MANAGER_BIT |
MANAGER_FEATURE_MATERIAL_MANAGER_BIT |
MANAGER_FEATURE_EVENT_BIT |
MANAGER_FEATURE_THREAD_GROUP_BIT |
MANAGER_FEATURE_COMMON_RENDERER_DATA_BIT |
MANAGER_FEATURE_UI_MANAGER_BIT |
MANAGER_FEATURE_AUDIO_MIXER_BIT |
MANAGER_FEATURE_AUDIO_BACKEND_BIT)
};
using ManagerFeatureFlags = uint32_t;
// Decouple creation from global TLS storage.
// This avoids some nasty cyclical dependencies.
class Factory
{
public:
virtual ~Factory() = default;
virtual FilesystemInterface *create_filesystem();
virtual AssetManagerInterface *create_asset_manager();
virtual MaterialManagerInterface *create_material_manager();
virtual EventManagerInterface *create_event_manager();
virtual ThreadGroupInterface *create_thread_group();
virtual CommonRendererDataInterface *create_common_renderer_data();
virtual PhysicsSystemInterface *create_physics_system();
virtual Audio::BackendInterface *create_audio_backend(Audio::MixerInterface *mixer,
float sample_rate,
unsigned channels);
virtual Audio::MixerInterface *create_audio_mixer();
virtual UI::UIManagerInterface *create_ui_manager();
virtual Util::MessageQueueInterface *create_message_queue();
};
void init(Factory &factory, ManagerFeatureFlags flags = MANAGER_FEATURE_DEFAULT_BITS,
unsigned max_threads = UINT_MAX, float audio_sample_rate = -1.0f);
void deinit();
// Used if the application wants to use multiple instances of Granite in the same process.
// This allows each thread to be associated to a global context.
struct GlobalManagers;
struct GlobalManagerDeleter
{
void operator()(GlobalManagers *managers);
};
using GlobalManagersHandle = std::unique_ptr<GlobalManagers, GlobalManagerDeleter>;
GlobalManagersHandle create_thread_context();
void delete_thread_context(GlobalManagers *managers);
void set_thread_context(const GlobalManagers &managers);
void clear_thread_context();
void start_audio_system();
void stop_audio_system();
void install_audio_system(Audio::BackendInterface *backend, Audio::MixerInterface *mixer);
Util::MessageQueueInterface *message_queue();
FilesystemInterface *filesystem();
AssetManagerInterface *asset_manager();
MaterialManagerInterface *material_manager();
EventManagerInterface *event_manager();
ThreadGroupInterface *thread_group();
UI::UIManagerInterface *ui_manager();
CommonRendererDataInterface *common_renderer_data();
Audio::BackendInterface *audio_backend();
Audio::MixerInterface *audio_mixer();
PhysicsSystemInterface *physics();
}
}
#define GRANITE_MESSAGE_QUEUE() static_cast<::Util::MessageQueue *>(::Granite::Global::message_queue())
#define GRANITE_FILESYSTEM() static_cast<::Granite::Filesystem *>(::Granite::Global::filesystem())
#define GRANITE_ASSET_MANAGER() static_cast<::Granite::AssetManager *>(::Granite::Global::asset_manager())
#define GRANITE_MATERIAL_MANAGER() static_cast<::Granite::MaterialManager *>(::Granite::Global::material_manager())
#define GRANITE_EVENT_MANAGER() static_cast<::Granite::EventManager *>(::Granite::Global::event_manager())
#define GRANITE_THREAD_GROUP() static_cast<::Granite::ThreadGroup *>(::Granite::Global::thread_group())
#define GRANITE_UI_MANAGER() static_cast<::Granite::UI::UIManager *>(::Granite::Global::ui_manager())
#define GRANITE_COMMON_RENDERER_DATA() static_cast<::Granite::CommonRendererData *>(::Granite::Global::common_renderer_data())
#define GRANITE_AUDIO_BACKEND() static_cast<::Granite::Audio::Backend *>(::Granite::Global::audio_backend())
#define GRANITE_AUDIO_MIXER() static_cast<::Granite::Audio::Mixer *>(::Granite::Global::audio_mixer())
#define GRANITE_PHYSICS() static_cast<::Granite::PhysicsSystem *>(::Granite::Global::physics())
@@ -0,0 +1,137 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "global_managers_init.hpp"
#include "global_managers.hpp"
#include "event.hpp"
#include "thread_group.hpp"
#include "filesystem.hpp"
#include "asset_manager.hpp"
#ifdef HAVE_GRANITE_RENDERER
#include "material_manager.hpp"
#include "common_renderer_data.hpp"
#include "ui_manager.hpp"
#endif
#ifdef HAVE_GRANITE_AUDIO
#include "audio_mixer.hpp"
#include "audio_interface.hpp"
#endif
#ifdef HAVE_GRANITE_PHYSICS
#include "physics_system.hpp"
#endif
namespace Granite
{
namespace Global
{
struct FactoryImplementation : Factory
{
FilesystemInterface *create_filesystem() override
{
return new Filesystem;
}
AssetManagerInterface *create_asset_manager() override
{
return new AssetManager;
}
EventManagerInterface *create_event_manager() override
{
return new EventManager;
}
ThreadGroupInterface *create_thread_group() override
{
return new ThreadGroup;
}
CommonRendererDataInterface *create_common_renderer_data() override
{
#ifdef HAVE_GRANITE_RENDERER
return new CommonRendererData;
#else
return nullptr;
#endif
}
UI::UIManagerInterface *create_ui_manager() override
{
#ifdef HAVE_GRANITE_RENDERER
return new UI::UIManager;
#else
return nullptr;
#endif
}
MaterialManagerInterface *create_material_manager() override
{
#ifdef HAVE_GRANITE_RENDERER
return new MaterialManager;
#else
return nullptr;
#endif
}
Audio::MixerInterface *create_audio_mixer() override
{
#ifdef HAVE_GRANITE_AUDIO
return new Audio::Mixer;
#else
return nullptr;
#endif
}
Audio::BackendInterface *create_audio_backend(Audio::MixerInterface *iface, float sample_rate, unsigned channels) override
{
#ifdef HAVE_GRANITE_AUDIO
if (iface)
return Audio::create_default_audio_backend(static_cast<Audio::Mixer *>(iface), sample_rate, channels);
else
return nullptr;
#else
(void)iface;
(void)sample_rate;
(void)channels;
return nullptr;
#endif
}
PhysicsSystemInterface *create_physics_system() override
{
#ifdef HAVE_GRANITE_PHYSICS
return new PhysicsSystem;
#else
return nullptr;
#endif
}
};
static FactoryImplementation factory;
void init(ManagerFeatureFlags flags, unsigned max_threads, float audio_sample_rate)
{
init(factory, flags, max_threads, audio_sample_rate);
}
}
}
@@ -0,0 +1,34 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "global_managers.hpp"
namespace Granite
{
namespace Global
{
void init(ManagerFeatureFlags flags = MANAGER_FEATURE_DEFAULT_BITS,
unsigned max_threads = UINT_MAX, float audio_sample_rate = -1.0f);
}
}
@@ -0,0 +1,133 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <functional>
#include <string>
#include "logging.hpp"
namespace Util
{
class MessageQueueInterface : public LoggingInterface
{
public:
virtual ~MessageQueueInterface() = default;
};
}
namespace Granite
{
class FilesystemInterface
{
public:
virtual ~FilesystemInterface() = default;
virtual bool load_text_file(const std::string &path, std::string &str) = 0;
};
class AssetManagerInterface
{
public:
virtual ~AssetManagerInterface() = default;
};
class MaterialManagerInterface
{
public:
virtual ~MaterialManagerInterface() = default;
virtual void iterate(AssetManagerInterface *iface) = 0;
};
class ThreadGroupInterface
{
public:
virtual ~ThreadGroupInterface() = default;
virtual void start(unsigned foreground_count, unsigned background_count,
const std::function<void()> &cb) = 0;
virtual void set_thread_context() = 0;
};
class EventManagerInterface
{
public:
virtual ~EventManagerInterface() = default;
};
class CommonRendererDataInterface
{
public:
virtual ~CommonRendererDataInterface() = default;
};
class PhysicsSystemInterface
{
public:
virtual ~PhysicsSystemInterface() = default;
};
class TouchDownEvent;
class TouchUpEvent;
class MouseMoveEvent;
class KeyboardEvent;
class OrientationEvent;
class TouchGestureEvent;
class MouseButtonEvent;
class JoypadButtonEvent;
class JoypadAxisEvent;
namespace UI
{
class UIManagerInterface
{
public:
virtual ~UIManagerInterface() = default;
virtual bool filter_input_event(const TouchDownEvent &e) = 0;
virtual bool filter_input_event(const TouchUpEvent &e) = 0;
virtual bool filter_input_event(const MouseMoveEvent &e) = 0;
virtual bool filter_input_event(const KeyboardEvent &e) = 0;
virtual bool filter_input_event(const OrientationEvent &e) = 0;
virtual bool filter_input_event(const TouchGestureEvent &e) = 0;
virtual bool filter_input_event(const MouseButtonEvent &e) = 0;
virtual bool filter_input_event(const JoypadButtonEvent &e) = 0;
virtual bool filter_input_event(const JoypadAxisEvent &e) = 0;
};
}
namespace Audio
{
class BackendInterface
{
public:
virtual ~BackendInterface() = default;
virtual bool start() = 0;
virtual bool stop() = 0;
};
class MixerInterface
{
public:
virtual ~MixerInterface() = default;
virtual void event_start(EventManagerInterface &event_manager) = 0;
virtual void event_stop(EventManagerInterface &event_manager) = 0;
};
}
}
@@ -0,0 +1,14 @@
add_granite_internal_lib(granite-input input.hpp input.cpp)
target_include_directories(granite-input PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-input PUBLIC granite-util granite-event granite-math)
if (${GRANITE_PLATFORM} MATCHES "SDL")
add_granite_internal_static_lib(granite-input-sdl input_sdl.cpp input_sdl.hpp)
target_link_libraries(granite-input-sdl PUBLIC granite-input)
if (GRANITE_SYSTEM_SDL)
find_package(SDL3 REQUIRED)
target_link_libraries(granite-input-sdl PUBLIC SDL3::SDL3-shared)
else()
target_link_libraries(granite-input-sdl PUBLIC SDL3-static)
endif()
endif()
@@ -0,0 +1,376 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "input.hpp"
#include "event.hpp"
#include "muglm/muglm_impl.hpp"
#include "logging.hpp"
#include <algorithm>
#include <string.h>
using namespace Util;
namespace Granite
{
const char *joypad_key_to_tag(JoypadKey key)
{
#define D(k) case JoypadKey::k: return #k
switch (key)
{
D(Left);
D(Right);
D(Up);
D(Down);
D(LeftShoulder);
D(RightShoulder);
D(West);
D(East);
D(North);
D(South);
D(LeftThumb);
D(RightThumb);
D(Mode);
D(Start);
D(Select);
default:
return "Unknown";
}
#undef D
}
const char *joypad_axis_to_tag(JoypadAxis axis)
{
#define D(k) case JoypadAxis::k: return #k
switch (axis)
{
D(LeftX);
D(LeftY);
D(RightX);
D(RightY);
D(LeftTrigger);
D(RightTrigger);
default:
return "Unknown";
}
#undef D
}
void InputTracker::orientation_event(quat rot)
{
OrientationEvent event(rot);
if (handler)
handler->dispatch(event);
}
void InputTracker::on_touch_down(unsigned id, float x, float y)
{
if (touch.active_pointers >= TouchCount)
{
LOGE("Touch pointer overflow!\n");
return;
}
unsigned index = touch.active_pointers++;
auto &pointer = touch.pointers[index];
pointer.id = id;
pointer.start_x = x;
pointer.start_y = y;
pointer.last_x = x;
pointer.last_y = y;
pointer.x = x;
pointer.y = y;
TouchDownEvent event(index, id, x, y, touch.width, touch.height);
if (handler)
handler->dispatch(event);
}
void InputTracker::dispatch_touch_gesture()
{
TouchGestureEvent event(touch);
if (handler)
handler->dispatch(event);
}
void InputTracker::on_touch_move(unsigned id, float x, float y)
{
auto &pointers = touch.pointers;
auto itr = std::find_if(std::begin(pointers), std::begin(pointers) + touch.active_pointers, [id](const TouchState::Pointer &pointer) {
return pointer.id == id;
});
if (itr == std::end(pointers))
{
LOGE("Could not find pointer!\n");
return;
}
itr->x = x;
itr->y = y;
}
void InputTracker::on_touch_up(unsigned id, float x, float y)
{
auto &pointers = touch.pointers;
auto itr = std::find_if(std::begin(pointers), std::begin(pointers) + touch.active_pointers, [id](const TouchState::Pointer &pointer) {
return pointer.id == id;
});
if (itr == std::end(pointers))
{
LOGE("Could not find pointer!\n");
return;
}
auto index = itr - std::begin(pointers);
TouchUpEvent event(itr->id, x, y, itr->start_x, itr->start_y, touch.width, touch.height);
if (handler)
handler->dispatch(event);
memmove(&pointers[index], &pointers[index + 1], (TouchCount - (index + 1)) * sizeof(TouchState::Pointer));
touch.active_pointers--;
}
void InputTracker::joypad_key_state(unsigned index, JoypadKey key, JoypadKeyState state)
{
if (index >= Joypads)
return;
assert(active_joypads & (1u << index));
auto &joy = joypads[index];
unsigned key_index = Util::ecast(key);
unsigned key_mask = 1u << key_index;
if (state == JoypadKeyState::Pressed)
{
if ((joy.button_mask & key_mask) == 0)
{
JoypadButtonEvent event(index, key, state);
if (handler)
handler->dispatch(event);
}
joy.button_mask |= key_mask;
}
else if (state == JoypadKeyState::Released)
{
if ((joy.button_mask & key_mask) != 0)
{
JoypadButtonEvent event(index, key, state);
if (handler)
handler->dispatch(event);
}
joy.button_mask &= ~key_mask;
}
}
void JoypadState::snap_deadzone(float deadzone)
{
memcpy(snapped_axis, raw_axis, sizeof(raw_axis));
static const JoypadAxis fused_axes[2][2] = {
{ JoypadAxis::LeftX, JoypadAxis::LeftY },
{ JoypadAxis::RightX, JoypadAxis::RightY },
};
for (auto &fused : fused_axes)
{
if (std::abs(raw_axis[int(fused[0])]) < deadzone && std::abs(raw_axis[int(fused[1])]) < deadzone)
for (auto &axis : fused)
snapped_axis[int(axis)] = 0.0f;
}
}
void InputTracker::joyaxis_state(unsigned index, JoypadAxis axis, float value)
{
if (index >= Joypads)
return;
assert(active_joypads & (1u << index));
auto &joy = joypads[index];
unsigned axis_index = Util::ecast(axis);
auto &a = joy.raw_axis[axis_index];
if (a != value)
{
JoypadAxisEvent event(index, axis, value);
if (handler)
handler->dispatch(event);
}
a = value;
}
void InputTracker::key_event(Key key, KeyState state)
{
if (state == KeyState::Released)
key_state &= ~(1ull << ecast(key));
else if (state == KeyState::Pressed)
key_state |= 1ull << ecast(key);
KeyboardEvent event(key, state);
if (handler)
handler->dispatch(event);
}
void InputTracker::mouse_button_event(Granite::MouseButton button, bool pressed)
{
mouse_button_event(button, last_mouse_x, last_mouse_y, pressed);
}
void InputTracker::mouse_button_event(MouseButton button, double x, double y, bool pressed)
{
if (pressed)
mouse_button_state |= 1ull << ecast(button);
else
mouse_button_state &= ~(1ull << ecast(button));
if (mouse_active)
{
last_mouse_x = x;
last_mouse_y = y;
}
MouseButtonEvent event(button, x, y, pressed);
if (handler)
handler->dispatch(event);
}
void InputTracker::mouse_move_event_relative(double x, double y)
{
x *= mouse_speed_x;
y *= mouse_speed_y;
if (mouse_active)
{
last_mouse_x += x;
last_mouse_y += y;
last_mouse_x = clamp(last_mouse_x, mouse_relative_range_x,
mouse_relative_range_x + mouse_relative_range_width);
last_mouse_y = clamp(last_mouse_y, mouse_relative_range_y,
mouse_relative_range_y + mouse_relative_range_height);
MouseMoveEvent event(x, y, last_mouse_x, last_mouse_y, key_state, mouse_button_state);
if (handler)
handler->dispatch(event);
}
}
void InputTracker::mouse_move_event_absolute(double x, double y)
{
if (mouse_active)
{
double delta_x = x - last_mouse_x;
double delta_y = y - last_mouse_y;
last_mouse_x = x;
last_mouse_y = y;
MouseMoveEvent event(delta_x, delta_y, x, y, key_state, mouse_button_state);
if (handler)
handler->dispatch(event);
}
}
void InputTracker::mouse_move_event_absolute_normalized(double x, double y)
{
mouse_move_event_absolute(x * double(touch.width), y * double(touch.height));
}
void InputTracker::mouse_button_event_normalized(MouseButton button, double x, double y, bool pressed)
{
mouse_button_event(button, x * double(touch.width), y * double(touch.height), pressed);
}
void InputTracker::mouse_enter(double x, double y)
{
mouse_active = true;
last_mouse_x = x;
last_mouse_y = y;
}
void InputTracker::mouse_leave()
{
mouse_active = false;
}
void InputTracker::dispatch_current_state(double delta_time, InputTrackerHandler *override_handler)
{
if (!override_handler)
override_handler = handler;
if (override_handler)
{
for (auto &pad : joypads)
pad.snap_deadzone(axis_deadzone);
override_handler->dispatch(JoypadStateEvent{active_joypads, joypads, Joypads, delta_time});
override_handler->dispatch(InputStateEvent{last_mouse_x, last_mouse_y,
delta_time, key_state, mouse_button_state, mouse_active});
}
}
int InputTracker::find_vacant_joypad_index() const
{
for (int i = 0; i < Joypads; i++)
{
if ((active_joypads & (1 << i)) == 0)
return i;
}
return -1;
}
void InputTracker::enable_joypad(unsigned index, uint32_t vid, uint32_t pid)
{
if (index >= Joypads)
return;
if (active_joypads & (1u << index))
return;
active_joypads |= 1u << index;
joypads[index] = {};
joypads[index].vid = vid;
joypads[index].pid = pid;
JoypadConnectionEvent event(index, true, vid, pid);
if (handler)
handler->dispatch(event);
}
void InputTracker::disable_joypad(unsigned index, uint32_t vid, uint32_t pid)
{
if (index >= Joypads)
return;
if ((active_joypads & (1u << index)) == 0)
return;
active_joypads &= ~(1u << index);
joypads[index] = {};
JoypadConnectionEvent event(index, false, vid, pid);
if (handler)
handler->dispatch(event);
}
std::mutex &InputTracker::get_lock()
{
return dispatch_lock;
}
}
@@ -0,0 +1,719 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "enum_cast.hpp"
#include "event.hpp"
#include <stdint.h>
#include "math.hpp"
#include <limits.h>
#include <float.h>
#include <mutex>
namespace Granite
{
enum class JoypadKey
{
Left,
Right,
Up,
Down,
East,
South,
West,
North,
LeftShoulder,
RightShoulder,
LeftThumb,
RightThumb,
Start,
Select,
Mode,
Count,
Unknown
};
const char *joypad_key_to_tag(JoypadKey key);
enum class JoypadAxis
{
LeftX,
LeftY,
RightX,
RightY,
LeftTrigger,
RightTrigger,
Count,
Unknown
};
const char *joypad_axis_to_tag(JoypadAxis axis);
enum class JoypadKeyState
{
Pressed,
Released,
Count
};
enum class Key
{
Unknown,
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
Return,
LeftCtrl,
LeftAlt,
LeftShift,
Space,
Escape,
Left, Right, Up, Down,
_1, _2, _3, _4, _5, _6, _7, _8, _9, _0,
Count
};
enum class MouseButton
{
Left,
Middle,
Right,
Count
};
enum class KeyState
{
Pressed,
Released,
Repeat,
Count
};
static_assert(Util::ecast(Key::Count) <= 64, "Cannot have more than 64 keys for bit-packing.");
struct TouchState
{
enum { PointerCount = 16 };
struct Pointer
{
unsigned id;
float start_x;
float start_y;
float last_x;
float last_y;
float x;
float y;
};
Pointer pointers[PointerCount] = {};
unsigned active_pointers = 0;
unsigned width;
unsigned height;
};
struct JoypadState
{
bool is_button_pressed(JoypadKey key) const
{
return (button_mask & (1u << Util::ecast(key))) != 0;
}
float get_axis(JoypadAxis a) const
{
return snapped_axis[Util::ecast(a)];
}
void snap_deadzone(float deadzone);
float raw_axis[Util::ecast(JoypadAxis::Count)] = {};
float snapped_axis[Util::ecast(JoypadAxis::Count)] = {};
uint32_t button_mask = 0;
uint32_t vid = 0;
uint32_t pid = 0;
};
static_assert(Util::ecast(JoypadKey::Count) <= 32, "Cannot have more than 32 joypad buttons.");
class InputTrackerHandler;
class InputTracker
{
public:
void key_event(Key key, KeyState state);
void mouse_button_event(MouseButton button, double x, double y, bool pressed);
void mouse_button_event_normalized(MouseButton button, double x, double y, bool pressed);
void mouse_button_event(MouseButton button, bool pressed);
void mouse_move_event_absolute(double x, double y);
void mouse_move_event_absolute_normalized(double x, double y);
void mouse_move_event_relative(double x, double y);
void dispatch_current_state(double delta_time, InputTrackerHandler *override_handler = nullptr);
void orientation_event(quat rot);
void joypad_key_state(unsigned index, JoypadKey key, JoypadKeyState state);
void joyaxis_state(unsigned index, JoypadAxis axis, float value);
void on_touch_down(unsigned id, float x, float y);
void on_touch_move(unsigned id, float x, float y);
void on_touch_up(unsigned id, float x, float y);
void mouse_enter(double x, double y);
void mouse_leave();
bool key_pressed(Key key) const
{
return (key_state & (1ull << Util::ecast(key))) != 0;
}
bool joykey_pressed(unsigned index, JoypadKey key) const
{
if (index >= Joypads)
return false;
return (joypads[index].button_mask & (1u << Util::ecast(key))) != 0;
}
bool mouse_button_pressed(MouseButton button) const
{
return (mouse_button_state & (1ull << Util::ecast(button))) != 0;
}
void dispatch_touch_gesture();
void set_axis_deadzone(float deadzone)
{
axis_deadzone = deadzone;
}
void set_relative_mouse_rect(double x, double y, double width, double height)
{
mouse_relative_range_x = x;
mouse_relative_range_y = y;
mouse_relative_range_width = width;
mouse_relative_range_height = height;
}
void set_relative_mouse_speed(double speed_x, double speed_y)
{
mouse_speed_x = speed_x;
mouse_speed_y = speed_y;
}
void enable_joypad(unsigned index, uint32_t vid, uint32_t pid);
void disable_joypad(unsigned index, uint32_t vid, uint32_t pid);
int find_vacant_joypad_index() const;
void set_touch_resolution(unsigned width, unsigned height)
{
touch.width = width;
touch.height = height;
}
void set_input_handler(InputTrackerHandler *handler_)
{
handler = handler_;
}
// To support dispatching input manager (i.e. polling) state from async threads.
std::mutex &get_lock();
enum { TouchCount = 16 };
enum { Joypads = 8 };
private:
InputTrackerHandler *handler = nullptr;
std::mutex dispatch_lock;
uint64_t key_state = 0;
uint8_t mouse_button_state = 0;
bool mouse_active = false;
double last_mouse_x = 0.0;
double last_mouse_y = 0.0;
double mouse_relative_range_x = 0.0;
double mouse_relative_range_y = 0.0;
double mouse_relative_range_width = DBL_MAX;
double mouse_relative_range_height = DBL_MAX;
double mouse_speed_x = 1.0;
double mouse_speed_y = 1.0;
uint8_t active_joypads = 0;
JoypadState joypads[Joypads] = {};
TouchState touch;
float axis_deadzone = 0.3f;
};
class JoypadConnectionEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadConnectionEvent)
JoypadConnectionEvent(unsigned index_, bool connected_, uint32_t vid_, uint32_t pid_)
: index(index_), connected(connected_), vid(vid_), pid(pid_)
{
}
unsigned get_index() const
{
return index;
}
bool is_connected() const
{
return connected;
}
uint32_t get_vid() const
{
return vid;
}
uint32_t get_pid() const
{
return pid;
}
private:
unsigned index;
bool connected;
uint32_t vid, pid;
};
class TouchGestureEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchGestureEvent)
explicit TouchGestureEvent(const TouchState &state_)
: state(state_)
{
}
const TouchState &get_state() const
{
return state;
}
private:
const TouchState &state;
};
class TouchDownEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchDownEvent)
TouchDownEvent(unsigned index_, unsigned id_,
float x_, float y_,
unsigned screen_width_, unsigned screen_height_)
: index(index_), id(id_),
x(x_), y(y_),
width(screen_width_), height(screen_height_)
{
}
float get_x() const
{
return x;
}
float get_y() const
{
return y;
}
unsigned get_index() const
{
return index;
}
unsigned get_id() const
{
return id;
}
unsigned get_screen_width() const
{
return width;
}
unsigned get_screen_height() const
{
return height;
}
private:
unsigned index, id;
float x, y;
unsigned width, height;
};
class TouchUpEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(TouchUpEvent)
TouchUpEvent(unsigned id_, float x_, float y_,
float start_x_, float start_y_,
unsigned screen_width_, unsigned screen_height_)
: id(id_), x(x_), y(y_),
start_x(start_x_), start_y(start_y_),
width(screen_width_), height(screen_height_)
{
}
float get_x() const
{
return x;
}
float get_y() const
{
return y;
}
float get_start_x() const
{
return start_x;
}
float get_start_y() const
{
return start_y;
}
unsigned get_id() const
{
return id;
}
unsigned get_screen_width() const
{
return width;
}
unsigned get_screen_height() const
{
return height;
}
private:
unsigned id;
float x, y;
float start_x, start_y;
unsigned width, height;
};
class JoypadButtonEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadButtonEvent)
JoypadButtonEvent(unsigned index_, JoypadKey key_, JoypadKeyState state_)
: index(index_), key(key_), state(state_)
{
}
unsigned get_index() const
{
return index;
}
JoypadKey get_key() const
{
return key;
}
JoypadKeyState get_state() const
{
return state;
}
private:
unsigned index;
JoypadKey key;
JoypadKeyState state;
};
class JoypadAxisEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadAxisEvent)
JoypadAxisEvent(unsigned index_, JoypadAxis axis_, float value_)
: index(index_), axis(axis_), value(value_)
{
}
unsigned get_index() const
{
return index;
}
JoypadAxis get_axis() const
{
return axis;
}
float get_value() const
{
return value;
}
private:
unsigned index;
JoypadAxis axis;
float value;
};
class KeyboardEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(KeyboardEvent)
KeyboardEvent(Key key_, KeyState state_)
: key(key_), state(state_)
{
}
Key get_key() const
{
return key;
}
KeyState get_key_state() const
{
return state;
}
private:
Key key;
KeyState state;
};
class OrientationEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(OrientationEvent)
explicit OrientationEvent(const quat &rot_)
: rot(rot_)
{
}
const quat &get_rotation() const
{
return rot;
}
private:
quat rot;
};
class MouseButtonEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(MouseButtonEvent)
MouseButtonEvent(MouseButton button_, double abs_x_, double abs_y_, bool pressed_)
: button(button_), abs_x(abs_x_), abs_y(abs_y_), pressed(pressed_)
{
}
MouseButton get_button() const
{
return button;
}
double get_abs_x() const
{
return abs_x;
}
double get_abs_y() const
{
return abs_y;
}
bool get_pressed() const
{
return pressed;
}
private:
MouseButton button;
double abs_x;
double abs_y;
bool pressed;
};
class MouseMoveEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(MouseMoveEvent);
MouseMoveEvent(double delta_x_, double delta_y_, double abs_x_, double abs_y_,
uint64_t key_mask_, uint8_t btn_mask_)
: delta_x(delta_x_), delta_y(delta_y_),
abs_x(abs_x_), abs_y(abs_y_),
key_mask(key_mask_), btn_mask(btn_mask_)
{
}
bool get_mouse_button_pressed(MouseButton button) const
{
return (btn_mask & (1 << Util::ecast(button))) != 0;
}
bool get_key_pressed(Key key) const
{
return (key_mask & (1ull << Util::ecast(key))) != 0;
}
double get_delta_x() const
{
return delta_x;
}
double get_delta_y() const
{
return delta_y;
}
double get_abs_x() const
{
return abs_x;
}
double get_abs_y() const
{
return abs_y;
}
private:
double delta_x, delta_y, abs_x, abs_y;
uint64_t key_mask;
uint8_t btn_mask;
};
class JoypadStateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(JoypadStateEvent)
JoypadStateEvent(uint8_t active_mask_, const JoypadState *states_,
unsigned count_, double delta_time_)
: states(states_), count(count_), delta_time(delta_time_), active_mask(active_mask_)
{
}
bool is_connected(unsigned index) const
{
if (index >= count)
return false;
return (active_mask & (1u << index)) != 0;
}
unsigned get_num_indices() const
{
return count;
}
const JoypadState &get_state(unsigned index) const
{
return states[index];
}
double get_delta_time() const
{
return delta_time;
}
private:
const JoypadState *states;
unsigned count;
double delta_time;
uint8_t active_mask;
};
class InputStateEvent : public Granite::Event
{
public:
GRANITE_EVENT_TYPE_DECL(InputStateEvent)
InputStateEvent(double abs_x_, double abs_y_,
double delta_time_, uint64_t key_mask_, uint8_t btn_mask_, bool mouse_active_)
: abs_x(abs_x_), abs_y(abs_y_),
delta_time(delta_time_), key_mask(key_mask_),
btn_mask(btn_mask_), mouse_active(mouse_active_)
{
}
double get_delta_time() const
{
return delta_time;
}
bool get_mouse_active() const
{
return mouse_active;
}
bool get_mouse_button_pressed(MouseButton button) const
{
return (btn_mask & (1 << Util::ecast(button))) != 0;
}
bool get_key_pressed(Key key) const
{
return (key_mask & (1ull << Util::ecast(key))) != 0;
}
double get_mouse_x() const
{
return abs_x;
}
double get_mouse_y() const
{
return abs_y;
}
private:
double abs_x, abs_y;
double delta_time;
uint64_t key_mask;
uint8_t btn_mask;
bool mouse_active;
};
class InputTrackerHandler
{
public:
virtual ~InputTrackerHandler() = default;
virtual void dispatch(const TouchDownEvent &e) = 0;
virtual void dispatch(const TouchUpEvent &e) = 0;
virtual void dispatch(const TouchGestureEvent &e) = 0;
virtual void dispatch(const JoypadButtonEvent &e) = 0;
virtual void dispatch(const JoypadAxisEvent &e) = 0;
virtual void dispatch(const KeyboardEvent &e) = 0;
virtual void dispatch(const OrientationEvent &e) = 0;
virtual void dispatch(const MouseButtonEvent &e) = 0;
virtual void dispatch(const MouseMoveEvent &e) = 0;
virtual void dispatch(const JoypadStateEvent &e) = 0;
virtual void dispatch(const InputStateEvent &e) = 0;
virtual void dispatch(const JoypadConnectionEvent &e) = 0;
};
}
@@ -0,0 +1,184 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "input_sdl.hpp"
namespace Granite
{
bool InputTrackerSDL::init(InputTracker &tracker, const Dispatcher &dispatcher)
{
// Open existing gamepads.
int num_pads = 0;
SDL_JoystickID *gamepad_ids = SDL_GetGamepads(&num_pads);
for (int i = 0; i < num_pads; i++)
add_gamepad(gamepad_ids[i], tracker, dispatcher);
if (gamepad_ids)
SDL_free(gamepad_ids);
// Poll these separately, inline in poll_input().
SDL_SetGamepadEventsEnabled(false);
SDL_SetJoystickEventsEnabled(false);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_ADDED, true);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_REMOVED, true);
SDL_SetEventEnabled(SDL_EVENT_JOYSTICK_UPDATE_COMPLETE, false);
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_UPDATE_COMPLETE, false);
return true;
}
void InputTrackerSDL::update(InputTracker &tracker)
{
SDL_UpdateGamepads();
for (int i = 0; i < int(InputTracker::Joypads); i++)
{
auto *pad = pads[i];
if (!pad)
continue;
static const struct
{
JoypadKey gkey;
SDL_GamepadButton sdl;
} buttons[] = {
{ JoypadKey::Left, SDL_GAMEPAD_BUTTON_DPAD_LEFT },
{ JoypadKey::Right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT },
{ JoypadKey::Up, SDL_GAMEPAD_BUTTON_DPAD_UP },
{ JoypadKey::Down, SDL_GAMEPAD_BUTTON_DPAD_DOWN },
{ JoypadKey::Start, SDL_GAMEPAD_BUTTON_START },
{ JoypadKey::Select, SDL_GAMEPAD_BUTTON_BACK },
{ JoypadKey::East, SDL_GAMEPAD_BUTTON_EAST },
{ JoypadKey::West, SDL_GAMEPAD_BUTTON_WEST },
{ JoypadKey::North, SDL_GAMEPAD_BUTTON_NORTH },
{ JoypadKey::South, SDL_GAMEPAD_BUTTON_SOUTH },
{ JoypadKey::LeftShoulder, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER },
{ JoypadKey::RightShoulder, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
{ JoypadKey::LeftThumb, SDL_GAMEPAD_BUTTON_LEFT_STICK },
{ JoypadKey::RightThumb, SDL_GAMEPAD_BUTTON_RIGHT_STICK },
{ JoypadKey::Mode, SDL_GAMEPAD_BUTTON_GUIDE },
};
for (auto &b : buttons)
{
tracker.joypad_key_state(i, b.gkey,
SDL_GetGamepadButton(pad, b.sdl) ?
JoypadKeyState::Pressed : JoypadKeyState::Released);
}
static const struct
{
JoypadAxis gaxis;
SDL_GamepadAxis sdl;
} axes[] = {
{ JoypadAxis::LeftX, SDL_GAMEPAD_AXIS_LEFTX },
{ JoypadAxis::LeftY, SDL_GAMEPAD_AXIS_LEFTY },
{ JoypadAxis::RightX, SDL_GAMEPAD_AXIS_RIGHTX },
{ JoypadAxis::RightY, SDL_GAMEPAD_AXIS_RIGHTY },
};
for (auto &a : axes)
{
float value = float(SDL_GetGamepadAxis(pad, a.sdl) - SDL_JOYSTICK_AXIS_MIN) /
float(SDL_JOYSTICK_AXIS_MAX - SDL_JOYSTICK_AXIS_MIN);
value = 2.0f * value - 1.0f;
tracker.joyaxis_state(i, a.gaxis, value);
}
tracker.joyaxis_state(i, JoypadAxis::LeftTrigger,
float(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) /
float(SDL_JOYSTICK_AXIS_MAX));
tracker.joyaxis_state(i, JoypadAxis::RightTrigger,
float(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) /
float(SDL_JOYSTICK_AXIS_MAX));
}
}
void InputTrackerSDL::close()
{
for (auto *pad : pads)
if (pad)
SDL_CloseGamepad(pad);
}
bool InputTrackerSDL::process_sdl_event(const SDL_Event &e, InputTracker &tracker,
const InputTrackerSDL::Dispatcher &dispatcher)
{
switch (e.type)
{
case SDL_EVENT_GAMEPAD_ADDED:
{
add_gamepad(e.gdevice.which, tracker, dispatcher);
return true;
}
case SDL_EVENT_GAMEPAD_REMOVED:
{
remove_gamepad(e.gdevice.which, tracker, dispatcher);
return true;
}
default:
break;
}
return false;
}
void InputTrackerSDL::add_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher)
{
int player_index = SDL_GetJoystickPlayerIndexForID(id);
if (player_index >= 0 && player_index < int(InputTracker::Joypads) && !pads[player_index])
{
uint32_t vid = SDL_GetGamepadVendorForID(id);
uint32_t pid = SDL_GetGamepadProductForID(id);
const char *name = SDL_GetGamepadNameForID(id);
LOGI("Plugging in controller: \"%s\" (%u/%u).\n", name, vid, pid);
const char *mapping = SDL_GetGamepadMappingForID(id);
LOGI(" Using mapping: \"%s\"\n", mapping);
pads[player_index] = SDL_OpenGamepad(id);
ids[player_index] = id;
dispatcher([player_index, vid, pid, &tracker]() {
tracker.enable_joypad(player_index, vid, pid);
});
}
}
void InputTrackerSDL::remove_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher)
{
for (int i = 0; i < int(InputTracker::Joypads); i++)
{
if (pads[i] && ids[i] == id)
{
uint32_t vid = SDL_GetGamepadVendor(pads[i]);
uint32_t pid = SDL_GetGamepadProduct(pads[i]);
SDL_CloseGamepad(pads[i]);
pads[i] = nullptr;
ids[i] = 0;
dispatcher([i, vid, pid, &tracker]() {
tracker.disable_joypad(i, vid, pid);
});
break;
}
}
}
}
@@ -0,0 +1,45 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <SDL3/SDL.h>
#include "input.hpp"
#include <functional>
namespace Granite
{
class InputTrackerSDL
{
public:
using Dispatcher = std::function<void (std::function<void ()>)>;
bool init(InputTracker &tracker, const Dispatcher &dispatcher);
void close();
bool process_sdl_event(const SDL_Event &event, InputTracker &tracker, const Dispatcher &dispatcher);
void update(InputTracker &tracker);
private:
SDL_Gamepad *pads[InputTracker::Joypads] = {};
SDL_JoystickID ids[InputTracker::Joypads] = {};
void add_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher);
void remove_gamepad(SDL_JoystickID id, InputTracker &tracker, const Dispatcher &dispatcher);
};
}
@@ -0,0 +1,53 @@
# Must be static due to Granite::application_create() feedback.
if (ANDROID)
add_granite_internal_static_lib(granite-platform application_android.cpp)
find_package(game-activity REQUIRED CONFIG)
find_package(games-controller REQUIRED CONFIG)
find_package(games-frame-pacing REQUIRED CONFIG)
target_link_libraries(granite-platform PRIVATE granite-rapidjson
game-activity::game-activity_static
games-controller::paddleboat games-frame-pacing::swappy)
target_compile_definitions(granite-platform PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
if (GRANITE_ANDROID_SWAPPY)
target_compile_definitions(granite-platform PRIVATE HAVE_SWAPPY)
endif()
if (GRANITE_ANDROID_APK_FILESYSTEM)
target_compile_definitions(granite-platform PRIVATE ANDROID_APK_FILESYSTEM)
endif()
elseif (${GRANITE_PLATFORM} MATCHES "libretro")
add_granite_internal_static_lib(granite-platform
application_libretro.cpp
application_libretro_utils.cpp
application_libretro_utils.hpp)
target_compile_definitions(granite-platform PUBLIC HAVE_LIBRETRO)
target_include_directories(granite-platform PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/libretro)
elseif (${GRANITE_PLATFORM} MATCHES "SDL")
add_granite_internal_static_lib(granite-platform application_sdl3.cpp)
if (GRANITE_SYSTEM_SDL)
find_package(SDL3 REQUIRED)
target_link_libraries(granite-platform PRIVATE SDL3::SDL3-shared)
else()
target_link_libraries(granite-platform PRIVATE SDL3-static)
if (NOT WIN32)
target_link_libraries(granite-platform PRIVATE dl)
endif()
endif()
target_link_libraries(granite-platform PRIVATE granite-input-sdl)
elseif (${GRANITE_PLATFORM} MATCHES "headless")
add_granite_internal_static_lib(granite-platform application_headless_wrapper.cpp)
elseif (${GRANITE_PLATFORM} MATCHES "null")
add_granite_internal_static_lib(granite-platform application_null.cpp)
else()
message(FATAL "GRANITE_PLATFORM is not set.")
endif()
if (NOT (${GRANITE_PLATFORM} MATCHES "null"))
target_link_libraries(granite-platform PRIVATE granite-application-interface-query granite-application granite-input granite-application-global-init)
endif()
if (GRANITE_AUDIO)
target_link_libraries(granite-platform PRIVATE granite-audio)
endif()
@@ -0,0 +1,20 @@
apply plugin: 'com.android.library'
android {
namespace = 'net.themaister.granite'
compileSdkVersion = 34
buildFeatures.prefab = true
defaultConfig {
minSdkVersion = 26
targetSdkVersion = 34
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core:1.9.0'
implementation 'androidx.games:games-activity:4.0.0'
}
@@ -0,0 +1 @@
Place {arm64-v8a,armeabi-v7a}/libVkLayer_*.so here to have it bundled in the APK.
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="$$VERSION_CODE$$"
android:versionName="$$VERSION_NAME$$">
<!-- Require Vulkan 1.1 -->
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true"/>
<uses-feature android:name="android.hardware.vulkan.level" android:version="0" android:required="true"/>
<application android:label="@string/app_name" android:icon="@drawable/$$ICON$$">
<activity android:name="$$ACTIVITY_NAME$$"
android:theme="@style/Application.Fullscreen"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:exported="true"
android:label="@string/app_name">
<meta-data android:name="android.app.lib_name" android:value="$$NATIVE_TARGET$$" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
@@ -0,0 +1,100 @@
apply plugin: 'com.android.application'
android {
namespace = '$$NAMESPACE$$'
compileSdkVersion = 34
defaultConfig {
minSdkVersion = 26
targetSdkVersion = 34
ndk {
abiFilters = [$$ABIS$$]
}
}
ndkVersion = '29.0.14206865'
buildTypes {
debug {
externalNativeBuild {
cmake {
arguments = ["-DANDROID_TOOLCHAIN=clang",
"-DANDROID_STL=c++_shared",
"-DANDROID_ARM_MODE=arm",
"-DANDROID_CPP_FEATURES=exceptions",
"-DGRANITE_SHADER_COMPILER_OPTIMIZE=$$SHADER_OPTIMIZE$$",
"-DGRANITE_VULKAN_FOSSILIZE=$$FOSSILIZE$$",
"-DGRANITE_ANDROID_SWAPPY=$$SWAPPY$$",
"-DCMAKE_BUILD_TYPE=Debug",
"-DANDROID_PLATFORM=android-26",
"-DGRANITE_SHIPPING=ON",
"-DGRANITE_AUDIO=$$AUDIO$$",
"-DGRANITE_BULLET=$$PHYSICS$$",
"-DANDROID_ARM_NEON=ON",
"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
targets = ["$$TARGET$$"]
}
}
jniDebuggable = true
}
release {
externalNativeBuild {
cmake {
arguments = ["-DANDROID_TOOLCHAIN=clang",
"-DANDROID_STL=c++_shared",
"-DANDROID_ARM_MODE=arm",
"-DANDROID_CPP_FEATURES=exceptions",
"-DGRANITE_SHADER_COMPILER_OPTIMIZE=$$SHADER_OPTIMIZE$$",
"-DGRANITE_VULKAN_FOSSILIZE=$$FOSSILIZE$$",
"-DGRANITE_ANDROID_SWAPPY=$$SWAPPY$$",
"-DCMAKE_BUILD_TYPE=Release",
"-DANDROID_PLATFORM=android-26",
"-DGRANITE_SHIPPING=ON",
"-DGRANITE_AUDIO=$$AUDIO$$",
"-DGRANITE_BULLET=$$PHYSICS$$",
"-DANDROID_ARM_NEON=ON",
"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
targets = ["$$TARGET$$"]
}
}
debuggable = true
signingConfig = signingConfigs.debug
jniDebuggable = true
}
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
resources.srcDirs = ['res']
res.srcDirs = ['res']
assets.srcDirs = ['$$ASSETS$$', '$$GRANITE_ASSETS$$']
jniLibs.srcDirs = ['$$EXTERNAL_JNI$$']
$$GRANITE_AUX_ASSETS$$
}
}
externalNativeBuild {
cmake {
path = "$$CMAKELISTS$$"
version = "3.22.0+"
}
}
buildFeatures {
prefab = true
}
}
dependencies {
api project(':granite')
implementation 'androidx.games:games-activity:4.0.0'
implementation 'androidx.games:games-controller:2.0.2'
implementation 'androidx.games:games-frame-pacing:2.1.3'
$$EXTRA_DEPENDENCIES$$
}
@@ -0,0 +1 @@
android.useAndroidX=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

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