mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-12 13:35:35 -04:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a79bf1e79 | |||
| 20f786a43e | |||
| 4c2bb26df7 | |||
| 229c162580 | |||
| 6d4a7e55e9 | |||
| b6e3c50d88 | |||
| 287ee6b2ee | |||
| 7113584632 | |||
| 867ec7fbab | |||
| 157bbfb4c9 | |||
| 2fcbebcd51 | |||
| 50565b37fc | |||
| 84e7394d5b | |||
| 409b60836b | |||
| 75a249f3e1 | |||
| d9a978f21f | |||
| 3585ab8a59 | |||
| 7df07c6904 | |||
| 6849baac06 | |||
| f3ca94b338 | |||
| 9b3b827054 | |||
| 4de3c20431 | |||
| 2840843e3c | |||
| 312195920a | |||
| bc11bf3563 | |||
| 36635f8c62 | |||
| 6136d9bbf7 | |||
| 16c573a170 | |||
| e9e16a8ac1 | |||
| 4339e60ba6 | |||
| af5635dd42 | |||
| c876ea558f | |||
| 9b75dc5fd2 | |||
| 5c9c76cc0b | |||
| b474aafe9d | |||
| 84c5729c4e | |||
| e0ab36b7d9 | |||
| d40a30ee13 | |||
| 074b43a089 | |||
| 8e8dc305e6 | |||
| 41389f4a8e | |||
| 8145dec2a1 | |||
| 4089a80a17 | |||
| b772b6d952 | |||
| 42910ab2fd | |||
| f54892b2c2 | |||
| f32e069c4b | |||
| 09f087656a | |||
| fe15366912 | |||
| cfadf7607a | |||
| f81d25b425 | |||
| ebf6f31719 | |||
| 590d209f76 | |||
| ed21cd4fd0 | |||
| 277538bb81 | |||
| 5418b1831d | |||
| 2d4e69466b | |||
| 427dcfab82 | |||
| f5642f3073 | |||
| cc9c15de54 | |||
| 1fd8a2ca3c | |||
| 0c9c8795ce | |||
| 9eb9acfa11 | |||
| facbf35343 | |||
| 7a34830dc7 | |||
| e4557efb23 | |||
| 42e12eb5ab | |||
| 16cc37ca10 | |||
| 44cb2c84ba | |||
| 8e9d4d624a | |||
| db87b91954 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
Language: Cpp
|
||||
Standard: C++03
|
||||
Standard: c++20
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignConsecutiveAssignments: false
|
||||
|
||||
+145
-135
@@ -5,96 +5,8 @@ if (NOT CMAKE_BUILD_TYPE)
|
||||
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
|
||||
endif ()
|
||||
|
||||
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
|
||||
|
||||
if (DUSK_VERSION_OVERRIDE)
|
||||
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
set(DUSK_WC_REVISION "")
|
||||
set(DUSK_WC_BRANCH "")
|
||||
set(DUSK_WC_DATE "")
|
||||
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
|
||||
else ()
|
||||
# obtain revision info from git
|
||||
find_package(Git)
|
||||
if (GIT_FOUND)
|
||||
# make sure version information gets re-run when the current Git HEAD changes
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
|
||||
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
|
||||
|
||||
# defines DUSK_WC_REVISION
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_REVISION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DESCRIBE
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
|
||||
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
# remove the git hash, then collapse a clean "-0" suffix only
|
||||
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
|
||||
# defines DUSK_WC_BRANCH
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_BRANCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DATE
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
|
||||
OUTPUT_VARIABLE DUSK_WC_DATE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
else ()
|
||||
message(STATUS "Unable to find git, commit information will not be available")
|
||||
endif ()
|
||||
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
|
||||
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
|
||||
set(_ver_major ${CMAKE_MATCH_1})
|
||||
set(_ver_minor ${CMAKE_MATCH_2})
|
||||
set(_ver_patch ${CMAKE_MATCH_3})
|
||||
set(DUSK_VERSION_TWEAK "0")
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
endif ()
|
||||
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
|
||||
if(DUSK_VERSION_TWEAK GREATER 999)
|
||||
set(_tweak 999)
|
||||
else()
|
||||
set(_tweak ${DUSK_VERSION_TWEAK})
|
||||
endif()
|
||||
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
|
||||
math(EXPR DUSK_VERSION_CODE
|
||||
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
|
||||
else ()
|
||||
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
endif ()
|
||||
|
||||
endif ()
|
||||
|
||||
# Add version information to CI environment variables
|
||||
if(DEFINED ENV{GITHUB_ENV})
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
|
||||
endif()
|
||||
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
|
||||
include(cmake/DetectVersion.cmake)
|
||||
detect_version()
|
||||
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
||||
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
|
||||
if (APPLE)
|
||||
@@ -177,12 +89,12 @@ option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
|
||||
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
|
||||
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
|
||||
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
|
||||
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
|
||||
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
|
||||
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
|
||||
|
||||
# Edit & Continue
|
||||
if (MSVC)
|
||||
if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug"
|
||||
AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
||||
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue")
|
||||
endif ()
|
||||
if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue")
|
||||
@@ -299,7 +211,47 @@ FetchContent_Declare(json
|
||||
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
)
|
||||
FetchContent_MakeAvailable(cxxopts json)
|
||||
message(STATUS "dusklight: Fetching miniz")
|
||||
FetchContent_Declare(miniz
|
||||
URL https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
|
||||
set(_fetch_content_deps cxxopts json miniz)
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
message(STATUS "dusklight: Fetching funchook")
|
||||
# cmake/PatchFunchook.cmake patches funchook's cmake/capstone.cmake.in to inject a
|
||||
# PATCH_COMMAND into capstone's inner ExternalProject. That PATCH_COMMAND runs
|
||||
# cmake/PatchCapstone.cmake after capstone is cloned, which removes the
|
||||
# cmake_policy(SET CMP0048 OLD) line that CMake >= 3.27 rejects.
|
||||
# This is incredibly scuffed and we should probably think of a better way to do this
|
||||
set(CAPSTONE_FIX_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/PatchCapstone.cmake")
|
||||
FetchContent_Declare(funchook
|
||||
GIT_REPOSITORY https://github.com/kubo/funchook.git
|
||||
GIT_TAG v1.1.3
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
PATCH_COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=<SOURCE_DIR> -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/PatchFunchook.cmake
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
set(FUNCHOOK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(FUNCHOOK_BUILD_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(FUNCHOOK_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||
list(LENGTH CMAKE_OSX_ARCHITECTURES _osx_arch_count)
|
||||
if (_osx_arch_count EQUAL 1)
|
||||
list(GET CMAKE_OSX_ARCHITECTURES 0 _osx_arch)
|
||||
if (_osx_arch MATCHES "^(arm64|aarch64|ARM64)$")
|
||||
set(FUNCHOOK_CPU arm64 CACHE STRING "" FORCE)
|
||||
elseif (_osx_arch MATCHES "^(x86_64|AMD64|amd64|i[3-6]86|x86)$")
|
||||
set(FUNCHOOK_CPU x86 CACHE STRING "" FORCE)
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
list(APPEND _fetch_content_deps funchook)
|
||||
endif ()
|
||||
FetchContent_MakeAvailable(${_fetch_content_deps})
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
message(STATUS "dusklight: Fetching sentry-native")
|
||||
@@ -333,21 +285,7 @@ if(_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQU
|
||||
add_compile_options(-fsigned-char)
|
||||
endif()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
||||
set(PLATFORM_NAME win32)
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
|
||||
if (IOS)
|
||||
set(PLATFORM_NAME ios)
|
||||
elseif (TVOS)
|
||||
set(PLATFORM_NAME tvos)
|
||||
else ()
|
||||
set(PLATFORM_NAME macos)
|
||||
endif ()
|
||||
else ()
|
||||
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
|
||||
endif ()
|
||||
|
||||
configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_BINARY_DIR}/version.h)
|
||||
configure_version_header()
|
||||
|
||||
include(files.cmake)
|
||||
|
||||
@@ -363,22 +301,16 @@ set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
|
||||
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
|
||||
source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
|
||||
|
||||
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
|
||||
|
||||
set(GAME_INCLUDE_DIRS
|
||||
include
|
||||
src
|
||||
assets/GZ2E01 # TODO: make this dynamic if needed?
|
||||
libs/JSystem/include
|
||||
libs
|
||||
extern/aurora/include/dolphin
|
||||
extern
|
||||
${CMAKE_BINARY_DIR})
|
||||
include(cmake/GameABIConfig.cmake)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1)
|
||||
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
|
||||
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
|
||||
Threads::Threads zstd::libzstd)
|
||||
Threads::Threads zstd::libzstd dusklight_game_headers)
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
list(APPEND GAME_LIBS funchook-static)
|
||||
endif ()
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
list(APPEND GAME_LIBS sentry)
|
||||
@@ -446,8 +378,8 @@ if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
|
||||
endif ()
|
||||
|
||||
if(ANDROID)
|
||||
list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1)
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_CODE_MODS=1)
|
||||
endif ()
|
||||
|
||||
if (DUSK_PACKAGE_INSTALL)
|
||||
@@ -474,7 +406,7 @@ set(GAME_DEBUG_FILES
|
||||
set_source_files_properties(
|
||||
${GAME_DEBUG_FILES}
|
||||
PROPERTIES
|
||||
COMPILE_DEFINITIONS "$<$<CONFIG:Debug>:DEBUG=1>;$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>"
|
||||
COMPILE_DEFINITIONS "$<$<CONFIG:Debug>:DEBUG=1>"
|
||||
)
|
||||
|
||||
# game_base is for all other game code files
|
||||
@@ -488,16 +420,11 @@ set(GAME_BASE_FILES
|
||||
set_source_files_properties(
|
||||
${GAME_BASE_FILES}
|
||||
PROPERTIES
|
||||
COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0;$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>"
|
||||
COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0"
|
||||
)
|
||||
|
||||
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||
target_compile_definitions(${jsystem_lib} PRIVATE
|
||||
${GAME_COMPILE_DEFS}
|
||||
$<$<CONFIG:Debug>:DEBUG=1>
|
||||
$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>
|
||||
)
|
||||
target_include_directories(${jsystem_lib} PRIVATE ${GAME_INCLUDE_DIRS})
|
||||
target_compile_definitions(${jsystem_lib} PRIVATE ${GAME_COMPILE_DEFS} $<$<CONFIG:Debug>:DEBUG=1>)
|
||||
target_link_libraries(${jsystem_lib} PRIVATE ${GAME_LIBS})
|
||||
set_target_properties(${jsystem_lib} PROPERTIES FOLDER "JSystem")
|
||||
endforeach()
|
||||
@@ -509,7 +436,7 @@ if (CMAKE_CXX_LINK_GROUP_USING_RESCAN_SUPPORTED OR CMAKE_LINK_GROUP_USING_RESCAN
|
||||
set(JSYSTEM_LINK_LIBRARIES "$<LINK_GROUP:RESCAN,${JSYSTEM_LIBRARIES}>")
|
||||
endif ()
|
||||
|
||||
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES})
|
||||
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_SOURCE_DIR}/miniz.c)
|
||||
if(ANDROID)
|
||||
add_library(dusklight SHARED ${DUSK_FILES})
|
||||
set_target_properties(dusklight PROPERTIES OUTPUT_NAME main)
|
||||
@@ -521,9 +448,55 @@ if (ENABLE_ASAN)
|
||||
endif ()
|
||||
|
||||
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
|
||||
target_include_directories(dusklight PRIVATE ${GAME_INCLUDE_DIRS})
|
||||
target_include_directories(dusklight PRIVATE ${miniz_SOURCE_DIR})
|
||||
target_link_libraries(dusklight PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
|
||||
target_precompile_headers(dusklight PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
|
||||
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
include(cmake/SymbolManifest.cmake)
|
||||
if (WIN32)
|
||||
# Game ABI exports & import library for mod linking
|
||||
include(cmake/WindowsExports.cmake)
|
||||
setup_windows_exports(dusklight)
|
||||
endif ()
|
||||
# Post-link symbol manifest: hookable-surface name->address map keyed to the build.
|
||||
setup_symbol_manifest(dusklight)
|
||||
endif ()
|
||||
|
||||
# Hook reliability: guaranteed patchable entries on the game ABI surface, and no identical-code folding.
|
||||
if (MSVC)
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:/hotpatch>)
|
||||
endif ()
|
||||
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
|
||||
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN:16 /OPT:NOICF)
|
||||
else ()
|
||||
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN /OPT:NOICF)
|
||||
endif ()
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
|
||||
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=2,1>)
|
||||
else ()
|
||||
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=10,5>)
|
||||
endif ()
|
||||
endif ()
|
||||
if (DEFINED DUSK_PATCHABLE_ENTRY_FLAG)
|
||||
target_compile_options(dusklight PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||
endforeach()
|
||||
endif ()
|
||||
|
||||
if (WIN32)
|
||||
target_link_libraries(dusklight PRIVATE Psapi)
|
||||
endif ()
|
||||
if (APPLE)
|
||||
# Mods resolve game symbols from the executable at dlopen time.
|
||||
target_link_options(dusklight PRIVATE -Wl,-export_dynamic)
|
||||
elseif (UNIX AND NOT ANDROID)
|
||||
target_link_options(dusklight PRIVATE -rdynamic)
|
||||
endif ()
|
||||
|
||||
if (TARGET crashpad_handler)
|
||||
add_dependencies(dusklight crashpad_handler)
|
||||
add_custom_command(TARGET dusklight POST_BUILD
|
||||
@@ -542,6 +515,7 @@ endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1")
|
||||
target_link_libraries(dusklight PRIVATE dl)
|
||||
endif ()
|
||||
|
||||
if (NOT APPLE)
|
||||
@@ -582,6 +556,15 @@ if (WIN32)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
include(cmake/ModSDK.cmake)
|
||||
|
||||
if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
add_custom_target(dusklight_mods) # Aggregate target for all in-tree mods
|
||||
add_subdirectory(mods/template_mod)
|
||||
add_subdirectory(mods/ao_mod)
|
||||
add_subdirectory(mods/shadow_mod)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
if (IOS)
|
||||
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios)
|
||||
@@ -589,6 +572,7 @@ if (APPLE)
|
||||
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/tvos)
|
||||
else ()
|
||||
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos)
|
||||
set(DUSK_ENTITLEMENTS ${DUSK_RESOURCE_DIR}/Dusklight.entitlements)
|
||||
endif ()
|
||||
set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in)
|
||||
file(GLOB_RECURSE DUSK_RESOURCE_FILES
|
||||
@@ -608,8 +592,7 @@ if (APPLE)
|
||||
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
|
||||
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
|
||||
endforeach ()
|
||||
set_target_properties(
|
||||
dusklight PROPERTIES
|
||||
set(_apple_bundle_properties
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
|
||||
@@ -620,6 +603,31 @@ if (APPLE)
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
|
||||
)
|
||||
if (DUSK_ENTITLEMENTS)
|
||||
list(APPEND _apple_bundle_properties
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${DUSK_ENTITLEMENTS})
|
||||
endif ()
|
||||
if (NOT IOS AND NOT TVOS)
|
||||
list(APPEND _apple_bundle_properties
|
||||
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES")
|
||||
endif ()
|
||||
set_target_properties(dusklight PROPERTIES ${_apple_bundle_properties})
|
||||
|
||||
if (NOT IOS AND NOT TVOS AND NOT "${CMAKE_GENERATOR}" STREQUAL "Xcode")
|
||||
set(_sign_nested_commands)
|
||||
if (TARGET crashpad_handler)
|
||||
list(APPEND _sign_nested_commands
|
||||
COMMAND /usr/bin/codesign --force --sign -
|
||||
"$<TARGET_FILE_DIR:dusklight>/crashpad_handler")
|
||||
endif ()
|
||||
add_custom_command(TARGET dusklight POST_BUILD
|
||||
${_sign_nested_commands}
|
||||
COMMAND /usr/bin/codesign --force --sign - --entitlements
|
||||
"${DUSK_ENTITLEMENTS}" "$<TARGET_BUNDLE_DIR:dusklight>"
|
||||
COMMENT "Signing Dusklight.app with entitlements"
|
||||
VERBATIM
|
||||
)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (APPLE AND NOT IOS AND NOT TVOS)
|
||||
@@ -743,3 +751,5 @@ foreach (target IN LISTS BINARY_TARGETS)
|
||||
endif ()
|
||||
endforeach ()
|
||||
endforeach ()
|
||||
|
||||
install_bundled_mods()
|
||||
|
||||
+23
-3
@@ -72,7 +72,11 @@
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
},
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
||||
"DUSK_ENABLE_CODE_MODS": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
"vendor": {
|
||||
"microsoft.com/VisualStudioSettings/CMake/1.0": {
|
||||
@@ -158,7 +162,15 @@
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "cl",
|
||||
"CMAKE_CXX_COMPILER": "cl",
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
||||
"DUSK_ENABLE_CODE_MODS": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
"vendor": {
|
||||
"microsoft.com/VisualStudioSettings/CMake/1.0": {
|
||||
@@ -258,7 +270,11 @@
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
},
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
||||
"DUSK_ENABLE_CODE_MODS": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
"vendor": {
|
||||
"microsoft.com/VisualStudioSettings/CMake/1.0": {
|
||||
@@ -390,6 +406,10 @@
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
},
|
||||
"DUSK_ENABLE_CODE_MODS": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
|
||||
@@ -20,31 +20,17 @@ It aims to be as accurate as possible to the original while also providing new o
|
||||
> Dusklight does *not* provide any copyrighted assets. You must provide your own copy of the original game.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> At a minimum, Dusklight requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelihood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them.
|
||||
> At a minimum, Dusklight requires a GPU with support for D3D12, Vulkan 1.1+, or Metal. For older devices, best-effort support is provided for D3D11 and OpenGL ES (Android), but will not achieve full accuracy or performance. Your experience with specific hardware, operating systems, and drivers may vary.
|
||||
|
||||
### 1. Dump your game
|
||||
|
||||
You must dump your own copy of the game, please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to a `.rvz` to save space.
|
||||
You must dump your own copy of the game. Please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to `.rvz` to save space.
|
||||
|
||||
Currently, only the GameCube USA and EUR releases are supported. Support for other versions of the game is planned in the future.
|
||||
|
||||
### 2. Download [Dusklight](https://github.com/TwilitRealm/dusklight/releases)
|
||||
### 2. Install Dusklight
|
||||
|
||||
### 3. Setup the game
|
||||
**Windows / macOS / Linux**
|
||||
- Extract the .zip file
|
||||
- Launch Dusklight
|
||||
- Press **Select Disc Image** and provide the path to your supported game dump
|
||||
- Press **Play**!
|
||||
|
||||
**iOS**
|
||||
- Follow the [iOS setup guide](docs/ios-install-altstore.md)
|
||||
|
||||
**Android**
|
||||
- Install the Dusklight APK
|
||||
- Launch Dusklight
|
||||
- Press **Select Disc Image** and provide the path to your supported game dump
|
||||
- Press **Play**!
|
||||
Visit the [official installation guide](https://twilitrealm.dev/install/) for full instructions.
|
||||
|
||||
# Building
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt)
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
|
||||
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
|
||||
|
||||
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
|
||||
|
||||
macro(detect_version)
|
||||
if (DUSK_VERSION_OVERRIDE)
|
||||
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
set(DUSK_WC_REVISION "")
|
||||
set(DUSK_WC_BRANCH "")
|
||||
set(DUSK_WC_DATE "")
|
||||
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
|
||||
else ()
|
||||
# obtain revision info from git
|
||||
find_package(Git)
|
||||
if (GIT_FOUND)
|
||||
# make sure version information gets re-run when the current Git HEAD changes
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
|
||||
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT}
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
|
||||
|
||||
# defines DUSK_WC_REVISION
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_REVISION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DESCRIBE
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
|
||||
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
# remove the git hash, then collapse a clean "-0" suffix only
|
||||
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
|
||||
# defines DUSK_WC_BRANCH
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_BRANCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DATE
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
|
||||
OUTPUT_VARIABLE DUSK_WC_DATE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
else ()
|
||||
message(STATUS "Unable to find git, commit information will not be available")
|
||||
endif ()
|
||||
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
|
||||
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
|
||||
set(_ver_major ${CMAKE_MATCH_1})
|
||||
set(_ver_minor ${CMAKE_MATCH_2})
|
||||
set(_ver_patch ${CMAKE_MATCH_3})
|
||||
set(DUSK_VERSION_TWEAK "0")
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
endif ()
|
||||
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
|
||||
if (DUSK_VERSION_TWEAK GREATER 999)
|
||||
set(_tweak 999)
|
||||
else ()
|
||||
set(_tweak ${DUSK_VERSION_TWEAK})
|
||||
endif ()
|
||||
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
|
||||
math(EXPR DUSK_VERSION_CODE
|
||||
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
|
||||
else ()
|
||||
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
endif ()
|
||||
|
||||
endif ()
|
||||
|
||||
# Add version information to CI environment variables
|
||||
if (DEFINED ENV{GITHUB_ENV})
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
|
||||
endif ()
|
||||
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
|
||||
endmacro()
|
||||
|
||||
# Sets PLATFORM_NAME and configures version.h into the caller's binary dir.
|
||||
macro(configure_version_header)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
||||
set(PLATFORM_NAME win32)
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
|
||||
if (IOS)
|
||||
set(PLATFORM_NAME ios)
|
||||
elseif (TVOS)
|
||||
set(PLATFORM_NAME tvos)
|
||||
else ()
|
||||
set(PLATFORM_NAME macos)
|
||||
endif ()
|
||||
else ()
|
||||
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
|
||||
endif ()
|
||||
|
||||
configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
endmacro()
|
||||
@@ -0,0 +1,32 @@
|
||||
# The game ABI surface shared by the main build and the mod SDK (sdk/CMakeLists.txt)
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_game_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
# PARTIAL_DEBUG makes debug and release share one struct/vtable ABI so a mod binary loads into either
|
||||
set(_game_compile_defs TARGET_PC=1 WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1 PARTIAL_DEBUG=1)
|
||||
if (ANDROID)
|
||||
list(APPEND _game_compile_defs TARGET_ANDROID=1)
|
||||
endif ()
|
||||
|
||||
set(_game_include_dirs
|
||||
${_game_root}/include
|
||||
${_game_root}/src
|
||||
${_game_root}/assets/GZ2E01 # TODO: make this dynamic if needed?
|
||||
${_game_root}/libs/JSystem/include
|
||||
${_game_root}/libs
|
||||
${_game_root}/extern/aurora/include/dolphin
|
||||
${_game_root}/extern/aurora/include
|
||||
${_game_root}/extern
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
# Interface target for mods and sub-projects to inherit game headers/defines.
|
||||
add_library(dusklight_game_headers INTERFACE)
|
||||
target_include_directories(dusklight_game_headers INTERFACE ${_game_include_dirs})
|
||||
target_compile_definitions(dusklight_game_headers INTERFACE ${_game_compile_defs})
|
||||
if (TARGET dawn::dawncpp_headers)
|
||||
target_link_libraries(dusklight_game_headers INTERFACE dawn::dawncpp_headers)
|
||||
elseif (TARGET dawn::webgpu_dawn)
|
||||
target_link_libraries(dusklight_game_headers INTERFACE dawn::webgpu_dawn)
|
||||
endif ()
|
||||
@@ -0,0 +1,295 @@
|
||||
# add_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>] [OVERLAY_DIR <overlay>]
|
||||
# [TEXTURES_DIR <textures>] [OUTPUT_DIR <dir>] [BUNDLE])
|
||||
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_BINARY_DIR}/mods" CACHE PATH "Directory to write mod packages into")
|
||||
|
||||
function(_mod_lib_name out_var)
|
||||
set(_arch "${CMAKE_SYSTEM_PROCESSOR}")
|
||||
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||
list(LENGTH CMAKE_OSX_ARCHITECTURES _count)
|
||||
if (_count GREATER 1)
|
||||
message(FATAL_ERROR "add_mod: universal binaries are not supported")
|
||||
endif ()
|
||||
set(_arch "${CMAKE_OSX_ARCHITECTURES}")
|
||||
endif ()
|
||||
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _platform)
|
||||
string(TOLOWER "${_arch}" _arch)
|
||||
if (_arch MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_arch "x86")
|
||||
endif ()
|
||||
if (WIN32)
|
||||
set(_ext ".dll")
|
||||
elseif (APPLE)
|
||||
set(_ext ".dylib")
|
||||
else ()
|
||||
set(_ext ".so")
|
||||
endif ()
|
||||
set(${out_var} "${_platform}-${_arch}${_ext}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_mod_resolve_source_path out_var path)
|
||||
if (IS_ABSOLUTE "${path}")
|
||||
set(_path "${path}")
|
||||
else ()
|
||||
set(_path "${CMAKE_CURRENT_SOURCE_DIR}/${path}")
|
||||
endif ()
|
||||
set(${out_var} "${_path}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_mod_collect_assets out_var dir)
|
||||
if (NOT IS_DIRECTORY "${dir}")
|
||||
message(FATAL_ERROR "add_mod: asset directory does not exist: ${dir}")
|
||||
endif ()
|
||||
|
||||
file(GLOB_RECURSE _files CONFIGURE_DEPENDS LIST_DIRECTORIES false "${dir}/*")
|
||||
set(${out_var} ${_files} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(add_mod target_name)
|
||||
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR" "SOURCES" ${ARGN})
|
||||
if (NOT ARG_MOD_JSON)
|
||||
message(FATAL_ERROR "add_mod: MOD_JSON is required")
|
||||
endif ()
|
||||
_mod_resolve_source_path(_mod_json "${ARG_MOD_JSON}")
|
||||
if (NOT EXISTS "${_mod_json}")
|
||||
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
|
||||
endif ()
|
||||
|
||||
set(_has_lib FALSE)
|
||||
set(_lib_name "")
|
||||
if (ARG_SOURCES)
|
||||
set(_has_lib TRUE)
|
||||
add_library(${target_name} SHARED ${ARG_SOURCES})
|
||||
_mod_lib_name(_lib_name)
|
||||
set_target_properties(${target_name} PROPERTIES
|
||||
PREFIX ""
|
||||
C_VISIBILITY_PRESET hidden
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
VISIBILITY_INLINES_HIDDEN ON
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS OFF)
|
||||
target_compile_features(${target_name} PRIVATE cxx_std_20)
|
||||
target_link_libraries(${target_name} PRIVATE dusklight_game_headers)
|
||||
|
||||
if (NOT TARGET dusklight)
|
||||
# Apply global compile options for out-of-tree mod builds
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
target_compile_options(${target_name} PRIVATE
|
||||
-Wno-multichar -Wno-trigraphs -Wno-deprecated-declarations)
|
||||
elseif (APPLE)
|
||||
target_compile_options(${target_name} PRIVATE
|
||||
-Wno-declaration-after-statement -Wno-non-pod-varargs)
|
||||
elseif (MSVC)
|
||||
target_compile_options(${target_name} PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>"
|
||||
"$<$<COMPILE_LANGUAGE:C,CXX>:/utf-8>")
|
||||
endif ()
|
||||
# Use signed char on ARM to match the original game (and x86)
|
||||
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _mod_arch)
|
||||
if (_mod_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
|
||||
target_compile_options(${target_name} PRIVATE -fsigned-char)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
# Game symbols resolve against the host executable at dlopen time.
|
||||
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
|
||||
elseif (ANDROID)
|
||||
if (TARGET dusklight)
|
||||
target_link_libraries(${target_name} PRIVATE dusklight)
|
||||
elseif (DUSK_GAME_SOLIB)
|
||||
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_SOLIB}")
|
||||
else ()
|
||||
message(FATAL_ERROR "add_mod: DUSK_GAME_SOLIB is not set (libmain.so)")
|
||||
endif ()
|
||||
elseif (UNIX)
|
||||
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
|
||||
elseif (WIN32)
|
||||
# Link against the generated import library (game ABI surface). Function calls
|
||||
# resolve through import thunks. Data is toolchain dependent:
|
||||
# - clang-cl: lld's mingw mode auto-imports data references, fixed up at load by
|
||||
# the mod SDK's pseudo-relocation runtime (pseudo_reloc.cpp).
|
||||
# - cl (MSVC): only DUSK_GAME_DATA-annotated data is reachable. Un-annotated
|
||||
# references fail to link.
|
||||
if (NOT DUSK_GAME_IMPLIB)
|
||||
message(FATAL_ERROR "add_mod: DUSK_GAME_IMPLIB is not set.")
|
||||
endif ()
|
||||
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_IMPLIB}")
|
||||
set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||
target_compile_definitions(${target_name} PRIVATE _ITERATOR_DEBUG_LEVEL=0)
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
target_compile_options(${target_name} PRIVATE "$<$<COMPILE_LANGUAGE:C,CXX>:/clang:-mcmodel=large>")
|
||||
target_sources(${target_name} PRIVATE "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../sdk/pseudo_reloc.cpp")
|
||||
# lld mingw mode rewrites /DEFAULTLIB directives to -l style and skips %LIB%, so
|
||||
# the CRT libraries and search paths are spelled out explicitly.
|
||||
target_link_options(${target_name} PRIVATE -lldmingw /nodefaultlib /INCREMENTAL:NO)
|
||||
target_link_libraries(${target_name} PRIVATE
|
||||
msvcrt.lib msvcprt.lib vcruntime.lib ucrt.lib
|
||||
oldnames.lib uuid.lib kernel32.lib user32.lib)
|
||||
set(_lib_dirs "$ENV{LIB}")
|
||||
if ("${_lib_dirs}" STREQUAL "")
|
||||
message(FATAL_ERROR "add_mod: %LIB% is empty; configure from a VS dev shell")
|
||||
endif ()
|
||||
foreach (_libdir IN LISTS _lib_dirs)
|
||||
target_link_options(${target_name} PRIVATE "/libpath:${_libdir}")
|
||||
endforeach ()
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
set(_output_dir "${DUSK_MODS_OUTPUT_DIR}")
|
||||
if (ARG_OUTPUT_DIR)
|
||||
set(_output_dir "${ARG_OUTPUT_DIR}")
|
||||
endif ()
|
||||
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage")
|
||||
set(_out "${_output_dir}/${target_name}.dusk")
|
||||
|
||||
set(_zip_args "${_lib_name}" mod.json)
|
||||
set(_package_deps "${_mod_json}")
|
||||
set(_package_inputs "${_mod_json}")
|
||||
set(_extra_cmds "")
|
||||
set(_lib_copy_cmd "")
|
||||
set(_target_depend "")
|
||||
if (_has_lib)
|
||||
list(APPEND _zip_args "${_lib_name}")
|
||||
set(_lib_copy_cmd COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:${target_name}>" "${_stage}/${_lib_name}")
|
||||
set(_target_depend ${target_name})
|
||||
endif ()
|
||||
if (ARG_RES_DIR)
|
||||
_mod_resolve_source_path(_res_dir "${ARG_RES_DIR}")
|
||||
_mod_collect_assets(_res_deps "${_res_dir}")
|
||||
list(APPEND _package_deps ${_res_deps})
|
||||
list(APPEND _package_inputs "${_res_dir}" ${_res_deps})
|
||||
list(APPEND _zip_args res)
|
||||
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${_res_dir}" "${_stage}/res")
|
||||
endif ()
|
||||
if (ARG_OVERLAY_DIR)
|
||||
_mod_resolve_source_path(_overlay_dir "${ARG_OVERLAY_DIR}")
|
||||
_mod_collect_assets(_overlay_deps "${_overlay_dir}")
|
||||
list(APPEND _package_deps ${_overlay_deps})
|
||||
list(APPEND _package_inputs "${_overlay_dir}" ${_overlay_deps})
|
||||
list(APPEND _zip_args overlay)
|
||||
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${_overlay_dir}" "${_stage}/overlay")
|
||||
endif ()
|
||||
if (ARG_TEXTURES_DIR)
|
||||
_mod_resolve_source_path(_textures_dir "${ARG_TEXTURES_DIR}")
|
||||
_mod_collect_assets(_textures_deps "${_textures_dir}")
|
||||
list(APPEND _package_deps ${_textures_deps})
|
||||
list(APPEND _package_inputs "${_textures_dir}" ${_textures_deps})
|
||||
list(APPEND _zip_args textures)
|
||||
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${_textures_dir}" "${_stage}/textures")
|
||||
endif ()
|
||||
|
||||
set(_bundle_cmds "")
|
||||
if (ARG_BUNDLE AND TARGET dusklight)
|
||||
file(READ "${_mod_json}" _mod_json_text)
|
||||
string(JSON _mod_id GET "${_mod_json_text}" id)
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_TARGETS "${target_name}")
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_IDS "${_mod_id}")
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_STAGES "${_stage}")
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_PACKAGES "${_out}")
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES "${_lib_name}")
|
||||
set(_bundle_cmds
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundled_mods"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_out}" "${CMAKE_BINARY_DIR}/bundled_mods/${target_name}.dusk")
|
||||
endif ()
|
||||
|
||||
set(_package_target "${target_name}_package")
|
||||
set(_package_inputs_file "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_package_inputs.txt")
|
||||
list(SORT _package_inputs)
|
||||
set(_package_inputs_text "")
|
||||
foreach (_package_input IN LISTS _package_inputs)
|
||||
string(APPEND _package_inputs_text "${_package_input}\n")
|
||||
endforeach ()
|
||||
file(GENERATE OUTPUT "${_package_inputs_file}" CONTENT "${_package_inputs_text}")
|
||||
add_custom_command(OUTPUT "${_out}"
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_stage}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${_output_dir}"
|
||||
${_lib_copy_cmd}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_mod_json}" "${_stage}/mod.json"
|
||||
${_extra_cmds}
|
||||
COMMAND ${CMAKE_COMMAND} -E chdir "${_stage}" ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
|
||||
${_bundle_cmds}
|
||||
DEPENDS ${_target_depend} ${_package_deps} "${_package_inputs_file}"
|
||||
COMMENT "Packaging ${target_name} -> ${_out}"
|
||||
COMMAND_EXPAND_LISTS
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(${_package_target} ALL DEPENDS "${_out}")
|
||||
if (TARGET dusklight_mods)
|
||||
add_dependencies(dusklight_mods ${_package_target})
|
||||
endif ()
|
||||
endfunction()
|
||||
|
||||
# Install rules for BUNDLE mods.
|
||||
# - Windows: the .dusk archives into <install>/mods (the loader extracts native libs to the
|
||||
# user cache).
|
||||
# - Linux: pre-extracted stage dirs into <install>/mods so native libs dlopen in place from
|
||||
# read-only installs.
|
||||
# - macOS: pre-extracted stage dirs into the installed app's Contents/Resources/mods, dylibs
|
||||
# ad-hoc signed in place, then the whole bundle re-signed.
|
||||
# - iOS/tvOS: assets into <app>/mods/<id> and the dylib into Frameworks/<id>.dylib.
|
||||
# - Android: nothing here; gradle packs ${CMAKE_BINARY_DIR}/bundled_mods into APK assets.
|
||||
function(install_bundled_mods)
|
||||
get_property(_targets GLOBAL PROPERTY DUSK_BUNDLED_MOD_TARGETS)
|
||||
if (NOT _targets OR ANDROID)
|
||||
return ()
|
||||
endif ()
|
||||
get_property(_ids GLOBAL PROPERTY DUSK_BUNDLED_MOD_IDS)
|
||||
get_property(_stages GLOBAL PROPERTY DUSK_BUNDLED_MOD_STAGES)
|
||||
get_property(_lib_names GLOBAL PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES)
|
||||
list(LENGTH _targets _count)
|
||||
math(EXPR _last "${_count} - 1")
|
||||
|
||||
if (APPLE)
|
||||
get_target_property(_app_name dusklight OUTPUT_NAME)
|
||||
if (NOT _app_name)
|
||||
set(_app_name dusklight)
|
||||
endif ()
|
||||
set(_bundle_dir "${CMAKE_INSTALL_PREFIX}/${_app_name}.app")
|
||||
if (IOS OR TVOS)
|
||||
foreach (_i RANGE ${_last})
|
||||
list(GET _targets ${_i} _target)
|
||||
list(GET _ids ${_i} _id)
|
||||
list(GET _stages ${_i} _stage)
|
||||
list(GET _lib_names ${_i} _lib_name)
|
||||
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/mods/${_id}"
|
||||
PATTERN "${_lib_name}" EXCLUDE)
|
||||
install(PROGRAMS "$<TARGET_FILE:${_target}>"
|
||||
DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.dylib")
|
||||
endforeach ()
|
||||
else ()
|
||||
foreach (_i RANGE ${_last})
|
||||
list(GET _ids ${_i} _id)
|
||||
list(GET _stages ${_i} _stage)
|
||||
list(GET _lib_names ${_i} _lib_name)
|
||||
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/Contents/Resources/mods/${_id}")
|
||||
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/Resources/mods/${_id}/${_lib_name}\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||
endforeach ()
|
||||
if (TARGET crashpad_handler)
|
||||
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/MacOS/$<TARGET_FILE_NAME:crashpad_handler>\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||
endif ()
|
||||
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - --entitlements \"${DUSK_ENTITLEMENTS}\" \"${_bundle_dir}\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||
endif ()
|
||||
return ()
|
||||
endif ()
|
||||
|
||||
if (DUSK_PACKAGE_INSTALL)
|
||||
set(_mods_dest "${CMAKE_INSTALL_DATAROOTDIR}/dusklight/mods")
|
||||
else ()
|
||||
set(_mods_dest "${CMAKE_INSTALL_PREFIX}/mods")
|
||||
endif ()
|
||||
if (WIN32)
|
||||
foreach (_target IN LISTS _targets)
|
||||
install(FILES "${CMAKE_BINARY_DIR}/bundled_mods/${_target}.dusk" DESTINATION "${_mods_dest}")
|
||||
endforeach ()
|
||||
else ()
|
||||
foreach (_i RANGE ${_last})
|
||||
list(GET _ids ${_i} _id)
|
||||
list(GET _stages ${_i} _stage)
|
||||
install(DIRECTORY "${_stage}/" DESTINATION "${_mods_dest}/${_id}")
|
||||
endforeach ()
|
||||
endif ()
|
||||
endfunction()
|
||||
@@ -0,0 +1,13 @@
|
||||
# Patches capstone's CMakeLists.txt for compatibility with CMake >= 4.0:
|
||||
# - Bumps cmake_minimum_required to 3.10 (CMake >= 4.0 dropped < 3.5 support; < 3.10 warns)
|
||||
# - Removes cmake_policy(SET CMP0048 OLD) (rejected by CMake >= 3.27)
|
||||
file(READ "${DIR}/CMakeLists.txt" _content)
|
||||
string(REGEX REPLACE
|
||||
"cmake_minimum_required[ \t]*\\([ \t]*VERSION[ \t]+[0-9]+\\.[0-9]+(\\.[0-9]+)?[ \t]*\\)"
|
||||
"cmake_minimum_required(VERSION 3.10)"
|
||||
_content "${_content}")
|
||||
string(REGEX REPLACE
|
||||
"cmake_policy[ \t]*\\([ \t]*SET[ \t]+CMP0048[ \t]+OLD[ \t]*\\)"
|
||||
"# cmake_policy(SET CMP0048 OLD)"
|
||||
_content "${_content}")
|
||||
file(WRITE "${DIR}/CMakeLists.txt" "${_content}")
|
||||
@@ -0,0 +1,60 @@
|
||||
file(READ "${SOURCE_DIR}/cmake/capstone.cmake.in" _content)
|
||||
|
||||
# Insert PATCH_COMMAND before CONFIGURE_COMMAND in the ExternalProject_Add.
|
||||
# Bracket args prevent cmake from substituting ${...} while writing this file.
|
||||
string(REPLACE
|
||||
" CONFIGURE_COMMAND \"\""
|
||||
[=[ PATCH_COMMAND "${CMAKE_COMMAND}" -DDIR=${CMAKE_CURRENT_BINARY_DIR}/capstone-src -P "${CAPSTONE_FIX_SCRIPT}"
|
||||
CONFIGURE_COMMAND ""]=]
|
||||
_content "${_content}")
|
||||
|
||||
file(WRITE "${SOURCE_DIR}/cmake/capstone.cmake.in" "${_content}")
|
||||
|
||||
file(READ "${SOURCE_DIR}/src/funchook_unix.c" _unix_content)
|
||||
|
||||
# macOS rejects the POSIX mprotect RWX/RW transition for executable image pages on arm64.
|
||||
# Use Mach VM_PROT_COPY for the short patch window, then restore RX permissions.
|
||||
if (NOT _unix_content MATCHES "VM_PROT_READ \\| VM_PROT_WRITE \\| VM_PROT_COPY")
|
||||
string(REPLACE
|
||||
[=[ rv = mprotect(mstate->addr, mstate->size, prot);]=]
|
||||
[=[#ifdef __APPLE__
|
||||
kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr,
|
||||
(vm_size_t)mstate->size, FALSE,
|
||||
VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY);
|
||||
if (kr == KERN_SUCCESS) {
|
||||
funchook_log(funchook, " unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR")\n",
|
||||
mstate->addr, mstate->size, start, len);
|
||||
return 0;
|
||||
}
|
||||
funchook_set_error_message(funchook, "Failed to unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR", error=%s)",
|
||||
mstate->addr, mstate->size, start, len,
|
||||
mach_error_string(kr));
|
||||
return FUNCHOOK_ERROR_MEMORY_FUNCTION;
|
||||
#endif
|
||||
rv = mprotect(mstate->addr, mstate->size, prot);]=]
|
||||
_unix_content "${_unix_content}")
|
||||
|
||||
string(REPLACE
|
||||
[=[ char errbuf[128];
|
||||
int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=]
|
||||
[=[ char errbuf[128];
|
||||
#ifdef __APPLE__
|
||||
kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr,
|
||||
(vm_size_t)mstate->size, FALSE,
|
||||
VM_PROT_READ | VM_PROT_EXECUTE);
|
||||
|
||||
if (kr == KERN_SUCCESS) {
|
||||
funchook_log(funchook, " protect memory %p (size=%"PRIuPTR", prot=read,exec)\n",
|
||||
mstate->addr, mstate->size);
|
||||
return 0;
|
||||
}
|
||||
funchook_set_error_message(funchook, "Failed to protect memory %p (size=%"PRIuPTR", prot=read,exec, error=%s)",
|
||||
mstate->addr, mstate->size,
|
||||
mach_error_string(kr));
|
||||
return FUNCHOOK_ERROR_MEMORY_FUNCTION;
|
||||
#endif
|
||||
int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=]
|
||||
_unix_content "${_unix_content}")
|
||||
endif ()
|
||||
|
||||
file(WRITE "${SOURCE_DIR}/src/funchook_unix.c" "${_unix_content}")
|
||||
@@ -0,0 +1,125 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||
|
||||
set(_SYMGEN_VERSION "1.1.1")
|
||||
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
|
||||
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
|
||||
mark_as_advanced(SYMGEN_PATH)
|
||||
|
||||
function(symgen_host_asset out_name)
|
||||
string(TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" _host_processor)
|
||||
set(_asset "")
|
||||
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
|
||||
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||
set(_asset "symgen-macos-arm64")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-macos-x86_64")
|
||||
endif ()
|
||||
elseif (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||
if (_host_processor MATCHES "^(aarch64|arm64)$")
|
||||
set(_asset "symgen-linux-aarch64")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-linux-x86_64")
|
||||
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_asset "symgen-linux-i686")
|
||||
endif ()
|
||||
elseif (CMAKE_HOST_WIN32)
|
||||
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||
set(_asset "symgen-windows-arm64.exe")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-windows-x86_64.exe")
|
||||
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_asset "symgen-windows-x86.exe")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
set(${out_name} "${_asset}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(ensure_symgen required)
|
||||
if (TARGET symgen)
|
||||
return()
|
||||
endif ()
|
||||
|
||||
if (SYMGEN_PATH)
|
||||
get_filename_component(_symgen "${SYMGEN_PATH}" ABSOLUTE)
|
||||
if (NOT EXISTS "${_symgen}")
|
||||
if (required)
|
||||
message(FATAL_ERROR "symgen: SYMGEN_PATH does not exist: ${_symgen}")
|
||||
endif ()
|
||||
message(STATUS "symgen: SYMGEN_PATH does not exist, symbol manifest generation "
|
||||
"skipped (by-name hook resolution will be unavailable)")
|
||||
return()
|
||||
endif ()
|
||||
else ()
|
||||
symgen_host_asset(_asset)
|
||||
if (_asset STREQUAL "")
|
||||
if (required)
|
||||
message(FATAL_ERROR "symgen: no prebuilt binary for host "
|
||||
"${CMAKE_HOST_SYSTEM_NAME}/${CMAKE_HOST_SYSTEM_PROCESSOR} "
|
||||
"(configure with -DDUSK_ENABLE_CODE_MODS=OFF)")
|
||||
endif ()
|
||||
message(STATUS "symgen: no prebuilt binary for host "
|
||||
"${CMAKE_HOST_SYSTEM_NAME}/${CMAKE_HOST_SYSTEM_PROCESSOR}; "
|
||||
"symbol manifest generation skipped (by-name hook resolution will be unavailable)")
|
||||
return()
|
||||
endif ()
|
||||
|
||||
set(_symgen_dir "${CMAKE_BINARY_DIR}/_deps/symgen")
|
||||
set(_symgen "${_symgen_dir}/${_asset}")
|
||||
set(_url "${_SYMGEN_RELEASE_BASE_URL}/${_asset}")
|
||||
message(STATUS "dusklight: Fetching symgen ${_SYMGEN_VERSION} (${_asset})")
|
||||
file(MAKE_DIRECTORY "${_symgen_dir}")
|
||||
file(DOWNLOAD "${_url}" "${_symgen}"
|
||||
TLS_VERIFY ON
|
||||
STATUS _download_status
|
||||
SHOW_PROGRESS)
|
||||
list(GET _download_status 0 _download_code)
|
||||
if (NOT _download_code EQUAL 0)
|
||||
list(GET _download_status 1 _download_message)
|
||||
file(REMOVE "${_symgen}")
|
||||
if (required)
|
||||
message(FATAL_ERROR "symgen: failed to download ${_url}: ${_download_message}")
|
||||
endif ()
|
||||
message(STATUS "symgen: failed to download ${_url}: ${_download_message}; "
|
||||
"symbol manifest generation skipped (by-name hook resolution will be unavailable)")
|
||||
return()
|
||||
endif ()
|
||||
if (NOT CMAKE_HOST_WIN32)
|
||||
file(CHMOD "${_symgen}" PERMISSIONS
|
||||
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_custom_target(symgen DEPENDS "${_symgen}")
|
||||
set(SYMGEN_EXE "${_symgen}" CACHE INTERNAL "symgen executable" FORCE)
|
||||
endfunction()
|
||||
|
||||
function(setup_symbol_manifest target)
|
||||
ensure_symgen(TRUE)
|
||||
if (NOT TARGET symgen)
|
||||
return()
|
||||
endif ()
|
||||
add_dependencies(${target} symgen)
|
||||
|
||||
if (WIN32)
|
||||
set(_input --pdb "$<TARGET_PDB_FILE:${target}>")
|
||||
set(_out "$<TARGET_FILE_DIR:${target}>/dusklight.symdb")
|
||||
else ()
|
||||
set(_input --binary "$<TARGET_FILE:${target}>")
|
||||
if (APPLE)
|
||||
set(_out "$<TARGET_BUNDLE_CONTENT_DIR:${target}>/Resources/dusklight.symdb")
|
||||
else ()
|
||||
set(_out "$<TARGET_FILE_DIR:${target}>/dusklight.symdb")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND "${SYMGEN_EXE}" manifest ${_input} --out "${_out}"
|
||||
COMMENT "Generating symbol manifest"
|
||||
VERBATIM)
|
||||
endfunction()
|
||||
@@ -0,0 +1,90 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_DUSK_WINDOWS_EXPORTS_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||
|
||||
# Windows mod linking: generate the curated export surface for the game executable and the
|
||||
# import library mods link against. symgen scans the built objects, filters by source, and
|
||||
# writes a .def used by the main link and import library generation.
|
||||
function(setup_windows_exports target)
|
||||
if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(WARNING "dusklight: Windows code-mod exports are x64-only for now; skipping")
|
||||
return()
|
||||
endif ()
|
||||
|
||||
include("${_DUSK_WINDOWS_EXPORTS_CMAKE_DIR}/SymbolManifest.cmake")
|
||||
ensure_symgen(TRUE)
|
||||
set(_symgen "${SYMGEN_EXE}")
|
||||
add_dependencies(${target} symgen)
|
||||
|
||||
set(_config_subdir "")
|
||||
if (CMAKE_CONFIGURATION_TYPES)
|
||||
set(_config_subdir "$<CONFIG>/")
|
||||
endif ()
|
||||
|
||||
set(_rsp_lines "$<TARGET_OBJECTS:${target}>")
|
||||
foreach (_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||
list(APPEND _rsp_lines "$<TARGET_FILE:${_lib}>")
|
||||
endforeach ()
|
||||
list(JOIN _rsp_lines "\n" _rsp_content)
|
||||
set(_rsp "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports_input.rsp")
|
||||
file(GENERATE OUTPUT "${_rsp}" CONTENT "${_rsp_content}")
|
||||
|
||||
set(_sdk_args)
|
||||
foreach (_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
|
||||
aurora_os aurora_pad aurora_si aurora_vi)
|
||||
if (TARGET ${_lib})
|
||||
list(APPEND _sdk_args --sdk-lib "$<TARGET_FILE:${_lib}>")
|
||||
endif ()
|
||||
endforeach ()
|
||||
|
||||
set(_forward_args)
|
||||
if (TARGET dawn::webgpu_dawn)
|
||||
get_target_property(_dawn_type dawn::webgpu_dawn TYPE)
|
||||
if (_dawn_type STREQUAL "SHARED_LIBRARY")
|
||||
list(APPEND _forward_args
|
||||
--forward-dll "$<TARGET_FILE:dawn::webgpu_dawn>"
|
||||
--forward-sym-prefix wgpu)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# Generate curated exports list from the main binary
|
||||
set(_def "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.def")
|
||||
add_custom_command(TARGET ${target} PRE_LINK
|
||||
# TODO: src/dusk/ is NOT excluded: inline code in game headers
|
||||
# currently call into it (e.g. dusk::frame_interp::lookup_replacement).
|
||||
COMMAND "${_symgen}" def
|
||||
--rsp "${_rsp}"
|
||||
--out "${_def}"
|
||||
--exclude cmake_pch
|
||||
--exclude miniz
|
||||
--exclude asan_options
|
||||
--max-exports 58000
|
||||
${_sdk_args}
|
||||
${_forward_args}
|
||||
COMMENT "Generating dusklight exports"
|
||||
VERBATIM)
|
||||
target_link_options(${target} PRIVATE "/DEF:${_def}")
|
||||
|
||||
# Generate import library for mods to link against.
|
||||
set(_implib "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_imports.lib")
|
||||
get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||
find_program(DUSK_LLVM_DLLTOOL llvm-dlltool HINTS "${_compiler_dir}")
|
||||
if (DUSK_LLVM_DLLTOOL)
|
||||
set(_implib_cmd "${DUSK_LLVM_DLLTOOL}" -d "${_def}" -D dusklight.exe -m i386:x86-64
|
||||
-l "${_implib}")
|
||||
else ()
|
||||
set(_implib_cmd "${CMAKE_AR}" /nologo "/def:${_def}" /machine:x64 /name:dusklight.exe
|
||||
"/out:${_implib}")
|
||||
endif ()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${_implib_cmd}
|
||||
BYPRODUCTS "${_implib}"
|
||||
COMMENT "Generating dusklight import library"
|
||||
VERBATIM)
|
||||
set(DUSK_GAME_IMPLIB "${_implib}" CACHE INTERNAL "Import library for Windows mod linking")
|
||||
set(DUSK_GAME_DEF "${_def}" CACHE INTERNAL "Curated export .def for the game executable")
|
||||
|
||||
# Ship the import library as sdk/dusklight.lib in the install tree: mods may use it to
|
||||
# compile against Dusklight without having to build the whole game. (See DUSK_GAME_IMPLIB)
|
||||
install(FILES "${_implib}" DESTINATION sdk RENAME dusklight.lib)
|
||||
endfunction()
|
||||
+764
@@ -0,0 +1,764 @@
|
||||
# Dusklight Mod API
|
||||
|
||||
Mods are `.dusk` bundles: zip archives that can contain code (in the form of native libraries), resources, DVD overlay
|
||||
files, and texture replacements. Mods may be enabled, disabled and reloaded at runtime.
|
||||
|
||||
When code mods are loaded, they get dynamically linked by the operating system to the running game process. The mod
|
||||
exports lifecycle functions that Dusklight calls into (`mod_initialize`, `mod_update`, `mod_shutdown`), and the mod
|
||||
communicates with the host via **services**: plain C APIs, individually versioned. Dusklight exports several built-in
|
||||
services, and mods may export services of their own, permitting framework mods and cross-mod integration.
|
||||
|
||||
Beyond services, mods have full access to the original game's code: include game headers, call directly into any public
|
||||
function, read and write data fields, and hook the vast majority of game functions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Getting Started](#getting-started)
|
||||
2. [mod.json](#modjson)
|
||||
3. [Anatomy of a Code Mod](#anatomy-of-a-code-mod)
|
||||
4. [Services](#services)
|
||||
5. [Built-in Services](#built-in-services)
|
||||
6. [Hooking Game Functions](#hooking-game-functions)
|
||||
7. [Asset Overlays](#asset-overlays)
|
||||
8. [Runtime Lifecycle](#runtime-lifecycle)
|
||||
9. [Error Handling](#error-handling)
|
||||
10. [Advanced: Exporting Services](#advanced-exporting-services)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
Fork the [mod template](../mods/template_mod/), a self-contained CMake project that uses the Dusklight mod SDK.
|
||||
|
||||
```
|
||||
my_mod/
|
||||
├── CMakeLists.txt
|
||||
├── mod.json
|
||||
├── src/mod.cpp
|
||||
├── res/ (optional bundled resources)
|
||||
├── overlay/ (optional game file overrides)
|
||||
└── textures/ (optional texture replacements)
|
||||
```
|
||||
|
||||
**CMakeLists.txt:**
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(my_mod CXX)
|
||||
|
||||
set(DUSKLIGHT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusklight" CACHE PATH "Path to dusklight source root")
|
||||
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
|
||||
|
||||
add_mod(my_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res # optional
|
||||
OVERLAY_DIR overlay # optional
|
||||
TEXTURES_DIR textures # optional
|
||||
)
|
||||
```
|
||||
|
||||
Building produces `my_mod.dusk` in `build/<preset>/mods/` (configurable via the `DUSK_MODS_OUTPUT_DIR` cache variable).
|
||||
Dusklight searches a `mods/` directory next to the app in addition to the user directory, so a dev build launched from
|
||||
`build/<preset>/` picks these up automatically: rebuild, relaunch (or click Reload), done.
|
||||
|
||||
For a regular game install, copy the `.dusk` into the user mods folder:
|
||||
|
||||
- Windows: `%APPDATA%\TwilitRealm\Dusklight\mods`
|
||||
- Linux: `~/.local/share/TwilitRealm/Dusklight/mods`
|
||||
- macOS: `~/Library/Application Support/TwilitRealm/Dusklight/mods`
|
||||
|
||||
Passing `--mods <dir>` on the command line replaces the user directory with one of your choosing.
|
||||
|
||||
---
|
||||
|
||||
## mod.json
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.example.my_mod",
|
||||
"name": "My Mod",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "A short description shown in the mod manager.",
|
||||
"icon": "res/my_icon.png",
|
||||
"banner": "res/my_banner.png"
|
||||
}
|
||||
```
|
||||
|
||||
`id` is required: a unique, stable identifier (reverse-DNS style; periods, underscores, and alphanumerics). Everything
|
||||
else is optional but recommended.
|
||||
|
||||
`icon` and `banner` are bundle-relative paths to PNG images for the in-game mod manager: the square icon (e.g.
|
||||
512x512), the banner (~3.5:1). If omitted, `res/icon.png` and `res/banner.png` are used automatically when present.
|
||||
|
||||
---
|
||||
|
||||
## Anatomy of a Code Mod
|
||||
|
||||
```cpp
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/log.h"
|
||||
|
||||
DEFINE_MOD(); // once, in exactly one translation unit
|
||||
IMPORT_SERVICE(LogService, svc_log); // resolved by the loader before mod_initialize
|
||||
|
||||
extern "C" {
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
svc_log->info(mod_ctx, "hello from my_mod");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError* error) { // called every frame
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError* error) {
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All three lifecycle exports are required. `mod_ctx` is your mod's identity token, set by the loader before
|
||||
`mod_initialize` runs. Pass it as the first argument to every service call.
|
||||
|
||||
---
|
||||
|
||||
## Services
|
||||
|
||||
A service is a struct of C function pointers with a version header. You declare what you use at file scope, and the
|
||||
loader resolves it before your mod initializes:
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(LogService, svc_log); // required, any minor version
|
||||
IMPORT_SERVICE_VERSION(LogService, svc_log, 2); // required, minor version >= 2
|
||||
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
|
||||
```
|
||||
|
||||
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
|
||||
allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major**
|
||||
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
|
||||
functions to the end of the struct without breaking existing callers and simply bumps the minor version. Mods that
|
||||
want the newer functions may use `IMPORT_SERVICE_VERSION` to require that minor at **load time**, or `SERVICE_HAS` to
|
||||
check at **runtime** whether a specific function is available.
|
||||
|
||||
The contract (see `include/mods/api.h` for the full version):
|
||||
|
||||
- **A required import is guaranteed valid.** If the service is missing or too old, the mod fails to load with a clear
|
||||
error. No need to null check at call sites.
|
||||
- **Anything at or below the minor version you imported can be called unconditionally.**
|
||||
- Optional imports may be null; check once in `mod_initialize`.
|
||||
- Fields newer than your imported minor must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a null
|
||||
check.
|
||||
|
||||
---
|
||||
|
||||
## Built-in Services
|
||||
|
||||
### LogService (`mods/svc/log.h`)
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
|
||||
svc_log->info(mod_ctx, "spawned the thing");
|
||||
svc_log->warn(mod_ctx, "that looks wrong");
|
||||
svc_log->error(mod_ctx, "very bad");
|
||||
svc_log->write(mod_ctx, LOG_LEVEL_DEBUG, "verbose details");
|
||||
```
|
||||
|
||||
Messages appear in the console prefixed with your mod ID. Messages are plain UTF-8 strings and are copied before the
|
||||
call returns; use `snprintf` or `fmt::format` for formatting.
|
||||
|
||||
### ResourceService (`mods/svc/resource.h`)
|
||||
|
||||
Loads files from the `res/` tree of your `.dusk` archive. Paths are relative to `res/` (pass `"config.txt"`, not
|
||||
`"res/config.txt"`); absolute paths and `..` are rejected.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(ResourceService, svc_resource);
|
||||
|
||||
ResourceBuffer buf = RESOURCE_BUFFER_INIT;
|
||||
if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
|
||||
// buf.data / buf.size
|
||||
svc_resource->free(mod_ctx, &buf);
|
||||
}
|
||||
```
|
||||
|
||||
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. Note that the bundle is read-only; for writable
|
||||
storage, use the directory from `svc_host->mod_dir(mod_ctx)`.
|
||||
|
||||
### HostService (`mods/svc/host.h`)
|
||||
|
||||
Mod metadata and runtime interaction with the loader:
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(HostService, svc_host);
|
||||
|
||||
const char* id = svc_host->mod_id(mod_ctx);
|
||||
const char* dir = svc_host->mod_dir(mod_ctx); // writable per-mod directory
|
||||
svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); // disables the mod
|
||||
```
|
||||
|
||||
`get_service`/`publish_service` provide dynamic service lookup; see [Advanced](#advanced-exporting-services).
|
||||
|
||||
**Lifecycle watches.** If your mod provides a service that hands out per-caller state (registrations, callbacks,
|
||||
handles), watch other mods' lifecycle and drop what you hold for a mod when it detaches.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE_VERSION(HostService, svc_host, 1);
|
||||
|
||||
void on_mod_lifecycle(ModContext* ctx, ModContext* subject, const char* subject_id,
|
||||
ModLifecycleEvent event, void* user_data) {
|
||||
if (event == MOD_LIFECYCLE_DETACHED) {
|
||||
drop_state_for(subject); // same ModContext* the subject passed into your service
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t watch = 0;
|
||||
svc_host->watch_mod_lifecycle(mod_ctx, on_mod_lifecycle, nullptr, &watch);
|
||||
```
|
||||
|
||||
`MOD_LIFECYCLE_DETACHED` fires on the game thread at a lifecycle safe point, after the subject's `mod_shutdown` ran and
|
||||
every service dropped its state. For your own mod's teardown, use `mod_shutdown` instead.
|
||||
|
||||
### HookService (`mods/svc/hook.h`)
|
||||
|
||||
Installs hooks on game functions and resolves symbols by name. You'll rarely call it directly; use the typed helpers in
|
||||
`mods/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||
|
||||
### OverlayService (`mods/svc/overlay.h`)
|
||||
|
||||
Registers DVD file overlays at runtime: the dynamic counterpart to the static `overlay/` directory (see
|
||||
[Asset Overlays](#asset-overlays)). Overlay a disc path with a file from your bundle, or with a caller-owned buffer
|
||||
(copied on registration):
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(OverlayService, svc_overlay);
|
||||
|
||||
OverlayHandle handle = 0;
|
||||
svc_overlay->add_file(mod_ctx, "/res/Msgus.arc", "res/replacement.arc", &handle);
|
||||
svc_overlay->add_buffer(mod_ctx, "/generated.txt", data, size, nullptr);
|
||||
svc_overlay->remove(mod_ctx, handle);
|
||||
```
|
||||
|
||||
`disc_path` must be absolute (leading `/`) and is matched against the disc case-insensitively. Paths that don't exist
|
||||
on the disc are added as new files. Changes are applied at the next frame boundary, and data the game already read
|
||||
stays in memory until the file is re-read: sometimes a scene reload, and in the worst case, a full restart.
|
||||
|
||||
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
|
||||
|
||||
### TextureService (`mods/svc/texture.h`)
|
||||
|
||||
Registers texture replacements at runtime: the dynamic counterpart to the static `textures/` directory (see
|
||||
[Asset Overlays](#asset-overlays)). Two forms: raw texel data with an explicit key, or an encoded `.dds`/`.png` from
|
||||
your bundle whose filename encodes the key:
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(TextureService, svc_texture);
|
||||
|
||||
// Encoded file; filename follows the replacement naming convention.
|
||||
TextureReplacementHandle handle = 0;
|
||||
svc_texture->register_file(mod_ctx, "res/tex1_32x32_$_6.png", &handle);
|
||||
|
||||
// Raw data: match by texel-data pointer or by content hash (TEXTURE_KEY_SOURCE).
|
||||
TextureKey key = TEXTURE_KEY_INIT;
|
||||
key.kind = TEXTURE_KEY_POINTER;
|
||||
key.pointer = someTexObj.data;
|
||||
TextureData data = TEXTURE_DATA_INIT;
|
||||
data.data = pixels; data.size = pixelsSize;
|
||||
data.width = 32; data.height = 32; data.gx_format = GX_TF_RGBA8_PC;
|
||||
svc_texture->register_data(mod_ctx, &key, &data, nullptr);
|
||||
|
||||
svc_texture->unregister(mod_ctx, handle);
|
||||
```
|
||||
|
||||
Filenames use the same Dolphin-style convention as the user's `texture_replacements` directory:
|
||||
`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, where hashes may be `$` (wildcard). `_mipN` sidecar files next to
|
||||
a registered file are picked up automatically. Files are decoded lazily on first use by the renderer; raw data is copied
|
||||
at registration. Registrations follow your mod's lifecycle.
|
||||
|
||||
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
|
||||
|
||||
### ConfigService (`mods/svc/config.h`)
|
||||
|
||||
Persistent, mod-scoped configuration variables. Each var is stored in the user's `config.json` under
|
||||
`mod.<escaped mod id>.<name>` (escaping: `.` → `_`, `_` → `__`, so `com.example.my_mod` becomes `com_example_my__mod`),
|
||||
next to the host's own settings:
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(ConfigService, svc_config);
|
||||
|
||||
ConfigVarDesc desc = CONFIG_VAR_DESC_INIT;
|
||||
desc.name = "speedMultiplier"; // 1-64 chars from [A-Za-z0-9_-]; "enabled" is reserved
|
||||
desc.type = CONFIG_VAR_FLOAT;
|
||||
desc.default_float = 1.0;
|
||||
ConfigVarHandle var = 0;
|
||||
svc_config->register_var(mod_ctx, &desc, &var);
|
||||
|
||||
double speed = 1.0;
|
||||
svc_config->get_float(mod_ctx, var, &speed);
|
||||
svc_config->set_float(mod_ctx, var, 2.0);
|
||||
|
||||
// Optional: get notified when the value changes.
|
||||
void on_speed_changed(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value,
|
||||
const ConfigVarValue* previous, void* user_data) {
|
||||
/* value->float_value is the new value, previous->float_value the old one */
|
||||
}
|
||||
svc_config->subscribe(mod_ctx, var, on_speed_changed, nullptr, nullptr);
|
||||
```
|
||||
|
||||
Types: `CONFIG_VAR_BOOL` (`bool`), `CONFIG_VAR_INT` (`int64_t`), `CONFIG_VAR_FLOAT` (`double`), `CONFIG_VAR_STRING`
|
||||
(UTF-8; `get_string` copies into a caller buffer, pass a `NULL` buffer with size 0 to query the length). Accessors are
|
||||
typed and must match the registration.
|
||||
|
||||
Change callbacks fire on the game thread whenever the value changes at runtime (your own `set_*` calls included).
|
||||
Writes that store the same value are silent. Values applied from `config.json` or `--cvar` at registration do
|
||||
**not** fire callbacks; read the value after `register_var` for the starting state.
|
||||
|
||||
### UiService (`mods/svc/ui.h`)
|
||||
|
||||
Integrate seamlessly with Dusklight's UI system: add controls and buttons to your mod's detail pane in the Mods window,
|
||||
create custom windows and modal dialogs, apply custom RCSS stylesheets (anywhere!), and add menu bar tabs.
|
||||
|
||||
**Mod panel:** Registers or replaces the panel rendered in your mod's detail pane; `build` runs every time the detail
|
||||
content is rebuilt, and `update` runs every frame while that mod is selected. While your mod is selected, the detail
|
||||
pane carries your mod's id as a `mod-id` attribute (like custom window roots), so scoped RCSS can target it (e.g.
|
||||
`[mod-id="com.example.mod"]`).
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(UiService, svc_ui);
|
||||
|
||||
UiElementHandle statusText = 0;
|
||||
|
||||
ModResult build(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
svc_ui->pane_add_section(mod_ctx, panel, "Status");
|
||||
svc_ui->pane_add_text(mod_ctx, panel, "starting...", &statusText);
|
||||
svc_ui->pane_add_progress(mod_ctx, panel, 0.5f, nullptr);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult update(ModContext*, void*, ModError*) {
|
||||
svc_ui->elem_set_text(mod_ctx, statusText, "running");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
UiModsPanelDesc panel = UI_MODS_PANEL_DESC_INIT;
|
||||
panel.build = build;
|
||||
panel.update = update;
|
||||
svc_ui->register_mods_panel(mod_ctx, &panel);
|
||||
```
|
||||
|
||||
Element setters must match the element kind: `elem_set_text`/`elem_set_rml` on text rows, and `elem_set_progress` on
|
||||
progress bars. `elem_set_class` sets or clears an RCSS class on any element handle, for styling via scoped or
|
||||
per-window RCSS. A non-`MOD_OK` result from `build`/`update` fails your mod, as do exceptions thrown from any UI
|
||||
callback.
|
||||
|
||||
**Controls:** `pane_add_control` adds an input row described by a `UiControlDesc`: `UI_CONTROL_BUTTON`,
|
||||
`UI_CONTROL_TOGGLE`, `UI_CONTROL_NUMBER`, `UI_CONTROL_STRING`, or `UI_CONTROL_SELECT`. Values bind with callbacks or
|
||||
directly to a config var.
|
||||
|
||||
```cpp
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_TOGGLE;
|
||||
control.label = "Enable rainbows";
|
||||
control.help_rml = "Shown in the help pane while focused.";
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = myBoolVar; // from svc_config->register_var
|
||||
svc_ui->pane_add_control(mod_ctx, leftPane, &control, nullptr);
|
||||
```
|
||||
|
||||
`UI_BINDING_CONFIG_VAR` wires persistence, change notifications, and the modified indicator automatically. The var
|
||||
type must match the control: `TOGGLE` = bool, `NUMBER` and `SELECT` = int, `STRING` = string. Float vars are not
|
||||
bindable; use callbacks and convert. `help_rml` and `SELECT` option lists render in a help pane, so `SELECT` controls
|
||||
are only available inside window tabs.
|
||||
|
||||
**Windows:** `window_push` pushes a tabbed two-pane window onto the document stack and shows it. Each tab's `build`
|
||||
receives the window handle plus fresh left and right pane handles on every activation. The optional per-tab `update`
|
||||
runs each frame while that tab is active. `on_closed` fires when the window is destroyed. `desc.rcss` optionally styles
|
||||
that window's document only; custom windows carry the owning mod's id as a `mod-id` attribute on the window root, so
|
||||
scoped RCSS can target your specific mod's windows (e.g. `window[mod-id="com.example.mod"]`).
|
||||
|
||||
```cpp
|
||||
UiTabDesc tabs[1] = {UI_TAB_DESC_INIT};
|
||||
tabs[0].title = "Options";
|
||||
tabs[0].build = build_options_tab;
|
||||
|
||||
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
|
||||
desc.tabs = tabs;
|
||||
desc.tab_count = 1;
|
||||
desc.on_closed = options_window_closed;
|
||||
UiWindowHandle window = 0;
|
||||
svc_ui->window_push(mod_ctx, &desc, &window);
|
||||
```
|
||||
|
||||
**Dialogs:** `dialog_push` shows a modal dialog. `variant` picks the style, `icon` optionally overrides the variant's
|
||||
default icon, and actions become buttons. After an action's `on_pressed` returns, the dialog closes unless the action
|
||||
sets `keep_open`. A `keep_open` action can close it later (or immediately) with `dialog_close`. Cancel fires
|
||||
`on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live
|
||||
dialog.
|
||||
|
||||
**Menu bar tabs:** `register_menu_tab` adds a tab to the in-game menu bar. `on_selected` fires when the user activates
|
||||
the tab: typically you'd push a window from it. The tab is removed by `unregister_menu_tab`, or automatically when the
|
||||
mod is disabled.
|
||||
|
||||
**Custom styles:** `register_styles(scope, rcss, &handle)` applies an RCSS stylesheet to every document of a scope:
|
||||
existing documents restyle immediately, and future ones pick it up when created. `register_styles_file(scope, path,
|
||||
&handle)` reads the sheet from your bundle's `res/` directory. Scopes are `UI_SCOPE_PRELAUNCH`, `UI_SCOPE_WINDOW`,
|
||||
`UI_SCOPE_MENU_BAR`, `UI_SCOPE_OVERLAY`, `UI_SCOPE_TOUCH_CONTROLS`, and `UI_SCOPE_GRAPHICS_TUNER`. Sheets apply after
|
||||
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
|
||||
unless changing host UI is intentional.
|
||||
|
||||
### GfxService (`mods/svc/gfx.h`)
|
||||
|
||||
Direct WebGPU access at various stages of the rendering pipeline. Mods use the `wgpu*` C API (via `webgpu/webgpu.h`) for
|
||||
custom draws and compute dispatches. Mods must manage their own WebGPU state, including pipelines and bind groups.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
|
||||
GfxDeviceInfo info = GFX_DEVICE_INFO_INIT;
|
||||
svc_gfx->get_device_info(mod_ctx, &info);
|
||||
```
|
||||
|
||||
`register_stage_hook` runs a game-thread callback during frame recording. The public stages are:
|
||||
|
||||
- `GFX_STAGE_SCENE_BEGIN`: world camera window after camera/projection/light setup
|
||||
- `GFX_STAGE_SCENE_AFTER_TERRAIN`: after terrain/shadow lists, before object and translucent lists
|
||||
- `GFX_STAGE_SCENE_AFTER_OPAQUE`: after sky/terrain/object opaque lists, before translucent lists
|
||||
- `GFX_STAGE_FRAME_BEFORE_HUD`: 3D scene and wipe are complete, before 2D/HUD lists
|
||||
- `GFX_STAGE_FRAME_AFTER_HUD`: full game scene, including HUD
|
||||
|
||||
Inside a stage callback, record work with `push_draw`, stream per-frame data with `push_verts`, `push_indices`,
|
||||
`push_uniform`, or `push_storage`, snapshot the current frame with `resolve_pass`, and use `create_pass`/`resolve_pass`
|
||||
for temporary offscreen passes. Draw callbacks run later on the render worker thread with the live
|
||||
`WGPURenderPassEncoder`; they may use only their `GfxDrawContext` handles and raw `wgpu*` calls. Compute callbacks
|
||||
registered with `register_compute_type` follow the same worker-thread rule and run on the frame command encoder.
|
||||
|
||||
All WGPU handles from the service are borrowed. Resolved target views are valid for the current frame only. GPU objects
|
||||
created by a mod are owned by that mod and should be released in `mod_shutdown`.
|
||||
|
||||
### CameraService (`mods/svc/camera.h`)
|
||||
|
||||
Converts a game view provided by a render callback into WebGPU-convention camera data. Matrix fields are column-major
|
||||
`float[16]` values using the matrix * column-vector convention (transpose of the game's row-major `Mtx`/`Mtx44` layout),
|
||||
ready to copy into WGSL `mat4x4f` uniforms.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(CameraService, svc_camera);
|
||||
|
||||
CameraInfo camera = CAMERA_INFO_INIT;
|
||||
if (svc_camera->get_camera(mod_ctx, game_view, &camera) == MOD_OK) {
|
||||
// camera.view_from_world, camera.proj_from_view, camera.eye, ...
|
||||
}
|
||||
```
|
||||
|
||||
`get_camera` returns `MOD_UNAVAILABLE` while the view is not a valid perspective camera, such as before the
|
||||
first in-game frame. Projection matrices match the renderer's WebGPU clip convention and renderer depth convention
|
||||
(reversed-Z by default).
|
||||
|
||||
---
|
||||
|
||||
## Hooking Game Functions
|
||||
|
||||
Mods may hook the vast majority of game functions, including file-local static, private and virtual functions.
|
||||
`mods/hook.hpp` provides typed helpers over the hook service:
|
||||
|
||||
```cpp
|
||||
#include "mods/hook.hpp"
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
```
|
||||
|
||||
### Pre-hooks
|
||||
|
||||
Run before the original. Return `HOOK_SKIP_ORIGINAL` to cancel it (post-hooks still run).
|
||||
|
||||
```cpp
|
||||
HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata) {
|
||||
daAlink_c* link = dusk::mods::arg<daAlink_c*>(args, 0); // arg 0 is `this`
|
||||
if (link->shape_angle.y > 10000) {
|
||||
return HOOK_SKIP_ORIGINAL;
|
||||
}
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
dusk::mods::hook_add_pre<&daAlink_c::posMove>(svc_hook, on_pos_move_pre);
|
||||
```
|
||||
|
||||
### Post-hooks
|
||||
|
||||
Run after the original (or after a replace-hook, or after a cancelled original). `retval` points to the return value,
|
||||
if any.
|
||||
|
||||
```cpp
|
||||
void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... }
|
||||
|
||||
dusk::mods::hook_add_post<&daAlink_c::posMove>(svc_hook, on_pos_move_post);
|
||||
```
|
||||
|
||||
### Replace-hooks
|
||||
|
||||
Substitute the original entirely. Call through to it via `Hook<...>::g_orig` if needed:
|
||||
|
||||
```cpp
|
||||
using ExecuteEntry = dusk::mods::Hook<&daAlink_c::execute>;
|
||||
|
||||
void on_execute_replace(ModContext*, void* args, void* retval, void*) {
|
||||
int result = ExecuteEntry::g_orig(dusk::mods::arg<daAlink_c*>(args, 0));
|
||||
if (retval != nullptr) {
|
||||
*static_cast<int*>(retval) = result;
|
||||
}
|
||||
}
|
||||
|
||||
dusk::mods::hook_replace<&daAlink_c::execute>(svc_hook, on_execute_replace);
|
||||
```
|
||||
|
||||
By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`,
|
||||
`userdata`) controls this and callback ordering. Multiple mods can attach pre/post hooks to the same function
|
||||
independently.
|
||||
|
||||
### Hooking by name
|
||||
|
||||
Functions you can't name in C++ (file-local statics, private class members, anything not in a header) can be hooked by
|
||||
symbol name instead. You must supply the signature along with the name.
|
||||
|
||||
```cpp
|
||||
// static callback used by Link's hookshot collider in d_a_alink_hook.inc
|
||||
using HookshotHit = dusk::mods::NamedHook<
|
||||
"daAlink_hookshotAtHitCallBack",
|
||||
void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>;
|
||||
|
||||
dusk::mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit_pre);
|
||||
...
|
||||
HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the original
|
||||
```
|
||||
|
||||
Class member functions must include `Class*` as the first argument.
|
||||
|
||||
The install fails with the resolve error when the name is missing (`MOD_UNAVAILABLE`), ambiguous (`MOD_CONFLICT`),
|
||||
or the manifest is absent (`MOD_UNSUPPORTED`). Unlike `Hook<&Class::method>`, the signature is **not**
|
||||
compiler-checked: a mismatched signature will corrupt the call.
|
||||
|
||||
### Reading and writing arguments
|
||||
|
||||
`args` is an array of pointers to the arguments. For member functions, index 0 is `this`; parameters follow in
|
||||
declaration order.
|
||||
|
||||
```cpp
|
||||
T value = dusk::mods::arg<T>(args, n); // copy
|
||||
T& ref = dusk::mods::arg_ref<T>(args, n); // read/write reference
|
||||
```
|
||||
|
||||
```cpp
|
||||
// fpc_ProcID fopAcM_createItem(..., int itemNo, ...): turn heart drops into green rupees
|
||||
HookAction on_create_item_pre(ModContext*, void* args, void*, void*) {
|
||||
int& itemNo = dusk::mods::arg_ref<int>(args, 1);
|
||||
if (itemNo == dItemNo_HEART_e) {
|
||||
itemNo = dItemNo_GREEN_RUPEE_e;
|
||||
}
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
dusk::mods::hook_add_pre<&fopAcM_createItem>(svc_hook, on_create_item_pre);
|
||||
```
|
||||
|
||||
For reference parameters (e.g. `const cXyz& pos`), `arg_ref<cXyz>` yields a direct reference.
|
||||
|
||||
### Resolving symbols by name
|
||||
|
||||
`resolve` looks a symbol up in the **symbol manifest**: a name→address map generated alongside every game build and
|
||||
keyed to that exact binary. It covers the whole image, including functions that aren't exported (file-local statics),
|
||||
which makes them hookable:
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
|
||||
void* addr = nullptr;
|
||||
uint32_t flags = 0;
|
||||
if (svc_hook->resolve(mod_ctx, "GXSetProjection", &addr, &flags) == MOD_OK) {
|
||||
// addr is the function's real address in the running game; hook or call it.
|
||||
}
|
||||
```
|
||||
|
||||
Two spellings work on every platform:
|
||||
|
||||
- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. They carry no
|
||||
signature, so overload sets (and file-local statics sharing a name) return `MOD_CONFLICT`.
|
||||
- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled spelling in
|
||||
dlsym convention (no Mach-O leading underscore). The escape hatch for overloads.
|
||||
|
||||
`MOD_UNSUPPORTED` means the manifest is missing or was built for a different game binary.
|
||||
|
||||
### Game code ABI contract
|
||||
|
||||
A primary consideration when letting mods link against the game is maintaining ABI stability across Dusklight
|
||||
versions. If your mod calls or hooks game code directly (anything beyond the service APIs), import `GameService`
|
||||
(`mods/svc/game.h`):
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(GameService, svc_game);
|
||||
```
|
||||
|
||||
Its major version is the game code ABI epoch: it's bumped when game struct or vtable layouts change incompatibly, and
|
||||
the ordinary service version check then rejects your mod with a clear error instead of letting it corrupt memory in a
|
||||
version it wasn't built for. Service-only and asset-only mods should *not* import it and will continue to work across
|
||||
game ABI changes.
|
||||
|
||||
The more you can do through services, the better: a mod that avoids touching game code directly sidesteps future ABI
|
||||
breaks entirely and plays nicer with other enabled mods.
|
||||
|
||||
---
|
||||
|
||||
## Asset Overlays
|
||||
|
||||
Files placed under `overlay/` in the `.dusk` archive override game files at the corresponding path, equivalent to
|
||||
replacing files in the .iso. This requires no code: an archive with just `mod.json` and `overlay/` is a complete mod.
|
||||
|
||||
Files placed under `textures/` register as texture replacements, and act just like the user's general
|
||||
`texture_replacements/` directory: Dolphin-style naming, matched by texture hash
|
||||
(`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, `$` as a hash wildcard). Subdirectories are scanned recursively;
|
||||
only the filename needs to match.
|
||||
|
||||
Both mechanisms are tied to the mod's lifecycle: disabling the mod removes its overrides (files revert to the disc
|
||||
contents on their next open; added files stop existing), and reloading serves the new bundle's content. However, game
|
||||
data the engine already read stays as-is until it is loaded again, which may require a scene change or, in the worst
|
||||
case, a full restart. Texture replacements usually take effect immediately.
|
||||
|
||||
If multiple sources replace the same file or texture, the last one wins: runtime registrations override static
|
||||
`textures/` or `overlay/` files, and later-loaded mods override earlier ones. Cross-mod conflicts log warnings.
|
||||
**All** mod-provided texture replacements override the user's `texture_replacements/`.
|
||||
|
||||
To configure overlays and texture replacements at runtime instead, see [OverlayService](#overlayservice-modssvcoverlayh)
|
||||
and [TextureService](#textureservice-modssvctextureh).
|
||||
|
||||
---
|
||||
|
||||
## Runtime Lifecycle
|
||||
|
||||
Mods can be disabled, re-enabled, and reloaded at runtime without restarting the game (the enabled state persists as the
|
||||
`mod.<escaped id>.enabled` config var). Write your mod assuming this happens:
|
||||
|
||||
- **Disable** calls `mod_shutdown`, removes your hooks, services, overlays, and texture replacements (both static and
|
||||
runtime-registered), and unloads your library.
|
||||
- **Enable** and **Reload** load a *fresh copy* of your library, imports are re-resolved, and `mod_initialize` runs
|
||||
again. You never see a second `mod_initialize` on the same image, so just make `mod_shutdown` release anything the
|
||||
loader doesn't manage for you (threads, files, game-side state you mutated).
|
||||
- **Reload** additionally re-reads the `.dusk` from disk, picking up a rebuilt library and changed assets. This is the
|
||||
fast iteration loop during development: rebuild, click Reload.
|
||||
|
||||
**Dependents restart too.** Disabling or reloading a mod that exports services shuts down the mods importing them
|
||||
first (in reverse dependency order) and brings them back afterward. A mod whose *required* provider is disabled stays
|
||||
suspended and resumes automatically when the provider returns. Mods with an *optional* import of a disabled provider
|
||||
restart with that import null.
|
||||
|
||||
**One caution for hooks:** lifecycle changes are applied between frames, which is safe for hooks on functions
|
||||
that return every frame (effectively everything you'd normally hook). Avoid hooking a function that stays on
|
||||
the stack for the whole session (e.g. the outermost main loop); a mod that does cannot be safely unloaded.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
Service calls report failure through `ModResult` return values (`MOD_OK`, `MOD_UNAVAILABLE`,
|
||||
`MOD_INVALID_ARGUMENT`, ...). Lifecycle exports additionally receive a `ModError*`: fill it (e.g. with
|
||||
`dusk::mods::set_error(error, code, "message")`) and return the code, and the loader disables the mod and shows the
|
||||
message to the user.
|
||||
|
||||
```cpp
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
if (!load_my_data()) {
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to load data");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
```
|
||||
|
||||
Throwing exceptions out of lifecycle functions also disables the mod (they are caught by the loader), but prefer
|
||||
explicit results.
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Exporting Services
|
||||
|
||||
Mods may export services of their own, permitting framework mods and cross-mod integration. Define the interface in a
|
||||
header both mods share:
|
||||
|
||||
```cpp
|
||||
// my_mod_api.h
|
||||
#include "mods/api.h"
|
||||
|
||||
#define MY_MOD_SERVICE_ID "com.example.my_mod.api"
|
||||
#define MY_MOD_SERVICE_MAJOR 1u
|
||||
#define MY_MOD_SERVICE_MINOR 0u
|
||||
|
||||
typedef struct MyModService {
|
||||
ServiceHeader header;
|
||||
ModResult (*do_thing)(ModContext* ctx, int value);
|
||||
} MyModService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<MyModService> {
|
||||
static constexpr const char* id = MY_MOD_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = MY_MOD_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
```
|
||||
|
||||
**Provider:**
|
||||
|
||||
```cpp
|
||||
ModResult do_thing(ModContext* ctx, int value) { ... }
|
||||
|
||||
constexpr MyModService g_service{
|
||||
.header = SERVICE_HEADER(MyModService, MY_MOD_SERVICE_MAJOR, MY_MOD_SERVICE_MINOR),
|
||||
.do_thing = do_thing,
|
||||
};
|
||||
EXPORT_SERVICE(g_service);
|
||||
```
|
||||
|
||||
**Consumer:**
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(MyModService, svc_my_mod);
|
||||
// or IMPORT_OPTIONAL_SERVICE if the dependency is optional
|
||||
|
||||
svc_my_mod->do_thing(mod_ctx, 42);
|
||||
```
|
||||
|
||||
The loader registers all exports before resolving any imports, so declaration order between mods doesn't matter. Note
|
||||
that the `ctx` a provider receives identifies the *calling* mod.
|
||||
|
||||
### Dependencies between mods
|
||||
|
||||
Service imports are also dependency declarations: the loader initializes mods in dependency order, so by the time your
|
||||
`mod_initialize` runs, every mod you import services from (required *or* optional) has already finished its own
|
||||
`mod_initialize`. This includes deferred services: a service the provider publishes during its initialization resolves
|
||||
into your import slot just like a static export.
|
||||
|
||||
Consequences of that contract:
|
||||
|
||||
- If a provider fails to load, every mod that *requires* one of its services is disabled too, with an error naming the
|
||||
provider. Optional imports of a failed provider simply resolve to `NULL`.
|
||||
- Mods whose **required** imports form a cycle all fail to load. If the cycle runs through an **optional** import, the
|
||||
loader breaks it there: the optional import still resolves, but its provider may not be initialized yet when you run.
|
||||
- `svc_host->get_service(...)` is outside this system. It sees whatever is published at call time and gives no
|
||||
initialization-order guarantee, which also makes it the escape hatch for intentionally cyclic designs.
|
||||
|
||||
Mods shut down in reverse initialization order, so services you import remain safe to call from `mod_shutdown`.
|
||||
|
||||
Rules for providers:
|
||||
|
||||
- Service IDs are global and use reverse-DNS names (e.g. `com.mydomain.mod.service`)
|
||||
- Every function pointer covered by your declared minor version must be populated.
|
||||
- Within a major version, only append fields; never reorder, remove, or repurpose them. Breaking changes require a major
|
||||
bump (which is, in effect, a new service).
|
||||
- Only one provider per `(id, major)` pair may be registered; duplicates are load errors.
|
||||
|
||||
For services whose construction can't happen at static-init time, declare the export with `EXPORT_DEFERRED_SERVICE(...)`
|
||||
and publish the pointer later via `svc_host->publish_service(...)`. Consumers can fetch services dynamically with
|
||||
`svc_host->get_service(...)`; prefer manifest imports whenever possible, since they give the loader dependency
|
||||
information and fail fast with good errors.
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: f49d3c5f58...1dde08fa0d
+38
@@ -1418,6 +1418,8 @@ set(DUSK_FILES
|
||||
include/dusk/scope_guard.hpp
|
||||
src/dusk/dvd_asset.cpp
|
||||
src/d/actor/d_a_alink_dusk.cpp
|
||||
src/dusk/android_frame_rate.hpp
|
||||
src/dusk/android_frame_rate.cpp
|
||||
src/dusk/asserts.cpp
|
||||
src/dusk/batch.cpp
|
||||
src/dusk/batch.hpp
|
||||
@@ -1494,6 +1496,12 @@ set(DUSK_FILES
|
||||
src/dusk/ui/input.hpp
|
||||
src/dusk/ui/icon_provider.cpp
|
||||
src/dusk/ui/icon_provider.hpp
|
||||
src/dusk/ui/logs_window.cpp
|
||||
src/dusk/ui/logs_window.hpp
|
||||
src/dusk/ui/mod_texture_provider.cpp
|
||||
src/dusk/ui/mod_texture_provider.hpp
|
||||
src/dusk/ui/mod_window.cpp
|
||||
src/dusk/ui/mod_window.hpp
|
||||
src/dusk/ui/modal.cpp
|
||||
src/dusk/ui/modal.hpp
|
||||
src/dusk/ui/nav_types.hpp
|
||||
@@ -1505,6 +1513,8 @@ set(DUSK_FILES
|
||||
src/dusk/ui/pane.hpp
|
||||
src/dusk/ui/menu_bar.cpp
|
||||
src/dusk/ui/menu_bar.hpp
|
||||
src/dusk/ui/mods_window.cpp
|
||||
src/dusk/ui/mods_window.hpp
|
||||
src/dusk/ui/prelaunch.cpp
|
||||
src/dusk/ui/prelaunch.hpp
|
||||
src/dusk/ui/preset.cpp
|
||||
@@ -1539,6 +1549,34 @@ set(DUSK_FILES
|
||||
src/dusk/OSReport.cpp
|
||||
src/dusk/OSThread.cpp
|
||||
src/dusk/OSMutex.cpp
|
||||
src/dusk/mods/log_buffer.cpp
|
||||
src/dusk/mods/log_buffer.hpp
|
||||
src/dusk/mods/manifest.cpp
|
||||
src/dusk/mods/manifest.hpp
|
||||
src/dusk/mods/loader/bundle_disk.cpp
|
||||
src/dusk/mods/loader/bundle_zip.cpp
|
||||
src/dusk/mods/loader/context.cpp
|
||||
src/dusk/mods/loader/depgraph.cpp
|
||||
src/dusk/mods/loader/depgraph.hpp
|
||||
src/dusk/mods/loader/loader.cpp
|
||||
src/dusk/mods/loader/loader.hpp
|
||||
src/dusk/mods/loader/native_module.cpp
|
||||
src/dusk/mods/loader/native_module.hpp
|
||||
src/dusk/mods/svc/camera.cpp
|
||||
src/dusk/mods/svc/config.cpp
|
||||
src/dusk/mods/svc/config.hpp
|
||||
src/dusk/mods/svc/game.cpp
|
||||
src/dusk/mods/svc/gfx.cpp
|
||||
src/dusk/mods/svc/hook.cpp
|
||||
src/dusk/mods/svc/host.cpp
|
||||
src/dusk/mods/svc/log.cpp
|
||||
src/dusk/mods/svc/overlay.cpp
|
||||
src/dusk/mods/svc/resource.cpp
|
||||
src/dusk/mods/svc/texture.cpp
|
||||
src/dusk/mods/svc/ui.cpp
|
||||
src/dusk/mods/svc/ui.hpp
|
||||
src/dusk/mods/svc/registry.cpp
|
||||
src/dusk/mods/svc/registry.hpp
|
||||
src/dusk/discord.cpp
|
||||
src/dusk/discord.hpp
|
||||
src/dusk/discord_presence.cpp
|
||||
|
||||
@@ -16,37 +16,37 @@
|
||||
];
|
||||
forAllSystems = lib.genAttrs supportedSystems;
|
||||
|
||||
dawnVersion = "v20260423.175430";
|
||||
nodVersion = "v2.0.0-alpha.8";
|
||||
dawnVersion = "v20260618.032059";
|
||||
nodVersion = "v2.0.0-alpha.10";
|
||||
versionSuffix = "nix-" + (self.shortRev or self.dirtyShortRev or "dirty");
|
||||
|
||||
dawnInfo = {
|
||||
"x86_64-linux" = {
|
||||
triple = "linux-x86_64";
|
||||
hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs=";
|
||||
hash = "sha256-GFSd573b+VQx/VmFdNQgWDd0V9ayQlcw0Zuopke12ak=";
|
||||
};
|
||||
"aarch64-linux" = {
|
||||
triple = "linux-aarch64";
|
||||
hash = "sha256-34yyFpfqBZUwoFXQ41F0AwAU78FaNihOSY0oriwn6B0=";
|
||||
hash = "sha256-ZaoP7BAjBMnfAv2/AMRi3FNH2ZtyqASCSFyU/oB2Mzg=";
|
||||
};
|
||||
"aarch64-darwin" = {
|
||||
triple = "darwin-arm64";
|
||||
hash = "sha256-eQnzrBp6gjiBek1VYQ9A5W13ClYWrDDKjIqv/7eNTR4=";
|
||||
hash = "sha256-HT+qtlLaSHyoXPrUcXgcTGa877X5YfzbxRD4bJb7i1Y=";
|
||||
};
|
||||
"x86_64-darwin" = {
|
||||
triple = "darwin-x86_64";
|
||||
hash = "sha256-QGWiGdxiI9kci3NPXH6QFFirxn16851zB/w3jqhIBJ4=";
|
||||
hash = "sha256-cUNaCbA7rlKSukDVKGaVEVw0Zt1+mSbaHbmUCMvMVWc=";
|
||||
};
|
||||
};
|
||||
|
||||
nodPrebuiltInfo = {
|
||||
"x86_64-linux" = {
|
||||
triple = "linux-x86_64";
|
||||
hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE=";
|
||||
hash = "sha256-FVQWECVA2gWdc+n5OQ/Tvwn8z0qdgjSd1WlFt5HKOec=";
|
||||
};
|
||||
"aarch64-darwin" = {
|
||||
triple = "macos-arm64";
|
||||
hash = "sha256-UPy1ywCcv0K6VJOU3uUelJuUdBh3UNaPRlyP5LOBeDw=";
|
||||
hash = "sha256-8ZEejxksVgShNKUVRCBYaLOp9x/qOC9pAeVrElQUGUk=";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
'';
|
||||
|
||||
dawn = pkgs.fetchzip {
|
||||
url = "https://github.com/encounter/dawn-build/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
|
||||
url = "https://github.com/encounter/dawn/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
|
||||
hash = dawnInfo.${system}.hash;
|
||||
stripRoot = false;
|
||||
};
|
||||
@@ -94,7 +94,7 @@
|
||||
owner = "encounter";
|
||||
repo = "nod";
|
||||
rev = nodVersion;
|
||||
hash = "sha256-+zrtVzjo0+X/6uMcNUn1+FaSR+jOhrcQSDNBFjw0NDs=";
|
||||
hash = "sha256-r8qDlOVxv5iKiFjJQrcBuL9HVoOM3yEjRVnQIMqaICs=";
|
||||
};
|
||||
patches = [ ./fix-cmake-paths.patch ];
|
||||
cargoDeps = pkgs.rustPlatform.importCargoLock {
|
||||
@@ -141,12 +141,12 @@
|
||||
XXHASH = pkgs.xxhash.src;
|
||||
ZSTD = pkgs.zstd.src;
|
||||
FMT = pkgs.fetchzip {
|
||||
url = "https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz";
|
||||
hash = "sha256-sUbxlYi/Aupaox3JjWFqXIjcaQa0LFjclQAOleT+FRA=";
|
||||
url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz";
|
||||
hash = "sha256-ZmI1Dv0ZabPlxa02OpERI47jp7zFfjpeWCy1WyuPYZ0=";
|
||||
};
|
||||
TRACY = pkgs.fetchzip {
|
||||
url = "https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz";
|
||||
hash = "sha256-hbNGOsGeyGSvCJ2No8RkwOib1lX2on3vNZSzyVkZdXw=";
|
||||
url = "https://github.com/wolfpld/tracy/archive/6789e7d6f9a65ec98926b602097a33a9676d2606.tar.gz";
|
||||
hash = "sha256-Xxyd7G/mnXEPpN+ehmwl0AkAhS3CwObpJNDgcqbdUJg=";
|
||||
};
|
||||
IMGUI = pkgs.fetchFromGitHub {
|
||||
owner = "ocornut";
|
||||
@@ -269,6 +269,12 @@
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postFixup = lib.optionalString (!isDarwin) ''
|
||||
patchelf \
|
||||
--add-needed "${pkgs.vulkan-loader}/lib/libvulkan.so" \
|
||||
$out/bin/dusklight
|
||||
'';
|
||||
|
||||
dontStrip = true;
|
||||
|
||||
meta = {
|
||||
|
||||
@@ -20,7 +20,7 @@ public:
|
||||
|
||||
/* 0x14 */ cM3dGAab mM3dGAab;
|
||||
/* 0x30 */ cBgS_ShdwDraw_Callback mCallbackFun;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x34 */ int field_0x34;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -48,6 +48,8 @@ public:
|
||||
/* 0x1370 */ Z2FxLineMgr mFxLineMgr;
|
||||
#if DEBUG
|
||||
/* 0x13BC */ Z2DebugSys mDebugSys;
|
||||
#elif PARTIAL_DEBUG
|
||||
alignas(Z2DebugSys) u8 mDebugSys[sizeof(Z2DebugSys)];
|
||||
#endif
|
||||
}; // Size: 0x138C
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ public:
|
||||
JAISoundHandle* getMainBgmHandle() { return &mMainBgmHandle; }
|
||||
JAISoundHandle* getSubBgmHandle() { return &mSubBgmHandle; }
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
f32 field_0x00_debug;
|
||||
u8 field_0x04_debug;
|
||||
#endif
|
||||
|
||||
@@ -100,13 +100,13 @@ public:
|
||||
bool isForceBattle() { return forceBattle_; }
|
||||
JSUList<Z2CreatureEnemy>* getEnemyList() { return &field_0x0; }
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
JSUList<Z2SoundObjBase>* getAllList() { return &allList_; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
/* 0x00 */ JSUList<Z2CreatureEnemy> field_0x0;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x0C */ JSUList<Z2SoundObjBase> allList_;
|
||||
#endif
|
||||
/* 0x0C */ Z2EnemyArea enemyArea_;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
struct Z2SoundStarter;
|
||||
|
||||
class Z2SoundObjBase : public Z2SoundHandles
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
, public JSULink<Z2SoundObjBase>
|
||||
#endif
|
||||
{
|
||||
|
||||
@@ -4556,6 +4556,7 @@ public:
|
||||
void handleWolfHowl();
|
||||
void handleQuickTransform();
|
||||
bool checkAimContext();
|
||||
bool checkAimInputContext();
|
||||
|
||||
void onIronBallChainInterpCallback();
|
||||
|
||||
|
||||
@@ -88,9 +88,14 @@ public:
|
||||
/* 0x396A */ u8 field_0x396A[0x399E - 0x396A];
|
||||
/* 0x399E */ s16 field_0x399e;
|
||||
/* 0x39A0 */ u8 field_0x39A0[0x39A4 - 0x39A0];
|
||||
|
||||
#if TARGET_PC
|
||||
/* 0x39A4 */ cM_rnd_c mMantRng;
|
||||
#endif
|
||||
};
|
||||
|
||||
#if TARGET_PC
|
||||
STATIC_ASSERT(sizeof(mant_class) == 0x39ac);
|
||||
#else
|
||||
STATIC_ASSERT(sizeof(mant_class) == 0x39a4);
|
||||
#endif
|
||||
|
||||
#endif /* D_A_MANT_H */
|
||||
|
||||
@@ -45,7 +45,7 @@ private:
|
||||
|
||||
class dAttParam_c : public JORReflexible {
|
||||
public:
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x04 */ s8 mHIOChildNo;
|
||||
#endif
|
||||
|
||||
@@ -66,7 +66,7 @@ public:
|
||||
/* 0x35 */ u8 mAttnCursorDisappearFrames;
|
||||
/* 0x38 */ f32 field_0x38;
|
||||
/* 0x3C */ f32 field_0x3c;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x44 */ s32 mDebugDispPosX;
|
||||
/* 0x48 */ s32 mDebugDispPosY;
|
||||
#endif
|
||||
|
||||
@@ -201,7 +201,7 @@ private:
|
||||
/* 0x02C */ u32 m_flags;
|
||||
/* 0x030 */ cXyz* pm_pos;
|
||||
/* 0x034 */ cXyz* pm_old_pos;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x038 */ cXyz unk_0x38;
|
||||
#endif
|
||||
/* 0x038 */ cXyz* pm_speed;
|
||||
@@ -229,7 +229,7 @@ private:
|
||||
/* 0x0CC */ f32 field_0xcc;
|
||||
/* 0x0D0 */ f32 m_wtr_chk_offset;
|
||||
/* 0x0D4 */ cBgS_PolyInfo* pm_out_poly_info;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x0E4 */ cXyz unk_0xe4;
|
||||
#endif
|
||||
/* 0x0D8 */ f32 field_0xd8;
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ public:
|
||||
|
||||
// /* 0x0000 */ cCcS mCCcS;
|
||||
/* 0x284C */ dCcMassS_Mng mMass_Mng;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x2AD0 */ u8 m_is_mass_all_timer;
|
||||
#endif
|
||||
}; // Size = 0x2AC4
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
#include <cstring>
|
||||
|
||||
#include "tracy/Tracy.hpp"
|
||||
#include "dusk/profiling.hpp"
|
||||
|
||||
enum dComIfG_ButtonStatus {
|
||||
/* 0x00 */ BUTTON_STATUS_NONE,
|
||||
@@ -1037,7 +1037,7 @@ public:
|
||||
/* 0x1DE09 */ u8 field_0x1de09;
|
||||
/* 0x1DE0A */ u8 field_0x1de0a;
|
||||
/* 0x1DE0B */ u8 mIsDebugMode;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x1DE0C */ OSStopwatch mStopwatch;
|
||||
#endif
|
||||
|
||||
@@ -1049,7 +1049,7 @@ public:
|
||||
|
||||
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
|
||||
|
||||
extern dComIfG_inf_c g_dComIfG_gameInfo;
|
||||
DUSK_GAME_EXTERN dComIfG_inf_c g_dComIfG_gameInfo;
|
||||
extern GXColor g_blackColor;
|
||||
extern GXColor g_clearColor;
|
||||
extern GXColor g_whiteColor;
|
||||
|
||||
@@ -41,7 +41,7 @@ public:
|
||||
BASE_ROOM5,
|
||||
BASE_DEMO,
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
BASE_DEBUG,
|
||||
#endif
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ public:
|
||||
/* 0x09B8 */ DUNGEON_LIGHT dungeonlight[8];
|
||||
/* 0x0C18 */ BOSS_LIGHT field_0x0c18[8];
|
||||
/* 0x0D58 */ BOSS_LIGHT field_0x0d58[6];
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x0E48 */ NAVYCHAN navy;
|
||||
/* 0x0E58 */ u8 field_0xe58[0xE68 - 0xE58]; // part of NAVYCHAN?
|
||||
#endif
|
||||
@@ -471,7 +471,7 @@ public:
|
||||
/* 0x130C */ u8 staffroll_next_timer;
|
||||
}; // Size: 0x1310
|
||||
|
||||
extern dScnKy_env_light_c g_env_light;
|
||||
DUSK_GAME_EXTERN dScnKy_env_light_c g_env_light;
|
||||
|
||||
STATIC_ASSERT(sizeof(dScnKy_env_light_c) == 4880);
|
||||
|
||||
|
||||
@@ -218,6 +218,7 @@ private:
|
||||
bool mCursorInterpPrevAngular;
|
||||
bool mCursorInterpCurrAngular;
|
||||
bool mCursorInterpInit;
|
||||
bool mPointerTouchPressHoveredCurrent;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -360,7 +360,12 @@ inline void dMsgObject_demoMessageGroup() {
|
||||
}
|
||||
|
||||
inline bool dMsgObject_isTalkNowCheck() {
|
||||
#if TARGET_PC
|
||||
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
|
||||
return msgObject != NULL && msgObject->getStatus() != 1;
|
||||
#else
|
||||
return dMsgObject_getMsgObjectClass()->getStatus() == 1 ? false : true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool dMsgObject_isKillMessageFlag() {
|
||||
@@ -497,7 +502,12 @@ inline void dMsgObject_onMsgSend() {
|
||||
}
|
||||
|
||||
inline bool dMsgObject_isFukidashiCheck() {
|
||||
#if TARGET_PC
|
||||
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
|
||||
return msgObject != NULL && msgObject->getScrnDrawPtr() != NULL;
|
||||
#else
|
||||
return dMsgObject_getMsgObjectClass()->getScrnDrawPtr() == NULL ? false : true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void* dMsgObject_getTalkHeap() {
|
||||
|
||||
@@ -521,13 +521,13 @@ private:
|
||||
/* 0x019 */ u8 field_0x19;
|
||||
/* 0x01A */ u8 field_0x1a;
|
||||
/* 0x01B */ u8 field_0x1b;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x01C */ dPa_simpleEcallBack field_0x1c[48];
|
||||
#else
|
||||
/* 0x01C */ dPa_simpleEcallBack field_0x1c[25];
|
||||
#endif
|
||||
/* 0x210 */ level_c field_0x210;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
u8 mSceneCount;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ private:
|
||||
/* 0x18 */ JKRHeap* heap;
|
||||
/* 0x1C */ JKRSolidHeap* mDataHeap;
|
||||
/* 0x20 */ void** mRes;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x24 */ int mSize;
|
||||
#endif
|
||||
}; // Size: 0x24
|
||||
|
||||
+4
-1
@@ -1008,7 +1008,7 @@ public:
|
||||
|
||||
static const int ZONE_MAX = 0x20;
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x000 */ u8 unk_0x0;
|
||||
/* 0x001 */ char unk_0x1;
|
||||
/* 0x000 */ u8 unk_0x2[0x48 - 0x2];
|
||||
@@ -1029,6 +1029,9 @@ public:
|
||||
/* 0xF30 */ s64 mSaveTotalTime;
|
||||
#if DEBUG
|
||||
/* 0xF80 */ flagFile_c mFlagFile;
|
||||
#elif PARTIAL_DEBUG
|
||||
// flagFile_c's ctor/virtuals are only defined under #if DEBUG (d_save.cpp)
|
||||
alignas(flagFile_c) u8 mFlagFile[sizeof(flagFile_c)];
|
||||
#endif
|
||||
}; // Size: 0xF38
|
||||
|
||||
|
||||
+4
-4
@@ -538,7 +538,7 @@ public:
|
||||
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const = 0;
|
||||
/* vt[87] */ virtual void setDoor(stage_tgsc_class*) = 0;
|
||||
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const = 0;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
virtual void setUnit(void*) = 0;
|
||||
virtual void* getUnit() = 0;
|
||||
#endif
|
||||
@@ -796,7 +796,7 @@ public:
|
||||
virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
||||
virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
||||
virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
virtual void setUnit(void* i_Unit) { mUnit = i_Unit; }
|
||||
virtual void* getUnit() { return mUnit; }
|
||||
#endif
|
||||
@@ -845,7 +845,7 @@ public:
|
||||
/* 0x54 */ stage_tgsc_class* mDrTg;
|
||||
/* 0x58 */ stage_tgsc_class* mDoor;
|
||||
/* 0x5C */ dStage_FloorInfo_c* mFloorInfo;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x60 */ void* mUnit;
|
||||
#endif
|
||||
/* 0x60 */ u16 mPlayerNum;
|
||||
@@ -990,7 +990,7 @@ public:
|
||||
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
||||
/* vt[87] */ virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
||||
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
virtual void setUnit(void* i_Unit) {
|
||||
UNUSED(i_Unit);
|
||||
OSReport("stage non unit list data !!\n");
|
||||
|
||||
@@ -97,6 +97,9 @@ public:
|
||||
private:
|
||||
#if DEBUG
|
||||
/* 0x00 */ dVibTest_c mVibTest;
|
||||
#elif PARTIAL_DEBUG
|
||||
// dVibTest_c's ctor/virtuals are only defined under #if DEBUG (d_vibration.cpp)
|
||||
alignas(dVibTest_c) u8 mVibTest[sizeof(dVibTest_c)];
|
||||
#endif
|
||||
|
||||
class {
|
||||
|
||||
+69
-7
@@ -1,9 +1,11 @@
|
||||
#ifndef DUSK_CONFIG_HPP
|
||||
#define DUSK_CONFIG_HPP
|
||||
|
||||
#include <concepts>
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <stdexcept>
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include "config_var.hpp"
|
||||
|
||||
namespace dusk::config {
|
||||
@@ -40,7 +42,7 @@ public:
|
||||
[[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0;
|
||||
};
|
||||
|
||||
template<ConfigValue T>
|
||||
template <ConfigValue T>
|
||||
class ConfigImpl : public ConfigImplBase {
|
||||
// Just downcasting the references...
|
||||
void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final {
|
||||
@@ -90,20 +92,28 @@ public:
|
||||
void Register(ConfigVarBase& configVar);
|
||||
|
||||
/**
|
||||
* \brief Indicate that all registrations have happened and everything should lock in.
|
||||
* \brief Unregister a CVar, detaching it from the config system.
|
||||
*
|
||||
* If the CVar carries a user-set value (Value or Speedrun layer), it is stashed as an
|
||||
* unregistered key: Save() keeps writing it, and a later Register() of the same name restores
|
||||
* it through the normal back-fill path. The CVar may be destroyed after this returns.
|
||||
*/
|
||||
void FinishRegistration();
|
||||
void unregister(ConfigVarBase& configVar);
|
||||
|
||||
/**
|
||||
* \brief Load config from the standard user preferences location.
|
||||
*/
|
||||
void LoadFromUserPreferences();
|
||||
void LoadFromFileName(const char* path);
|
||||
void load_from_user_preferences();
|
||||
void load_from_file_name(const char* path);
|
||||
|
||||
void load_arg_override(std::string_view name, std::string_view value);
|
||||
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* \brief Save the config to file.
|
||||
*/
|
||||
void Save();
|
||||
void save();
|
||||
|
||||
/**
|
||||
* \brief Get a registered CVar by name.
|
||||
@@ -124,6 +134,58 @@ void ClearAllActionBindings(int port);
|
||||
*/
|
||||
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
|
||||
|
||||
/**
|
||||
* \brief Type-erased change callback. previousValue points at the value before the mutation
|
||||
* (a `const T*` for a `ConfigVar<T>`) and is valid only for the duration of the call.
|
||||
*/
|
||||
using ChangeCallback = std::function<void(ConfigVarBase& cVar, const void* previousValue)>;
|
||||
|
||||
/**
|
||||
* \brief Token identifying a change subscription. 0 is never a valid token.
|
||||
*/
|
||||
using Subscription = u64;
|
||||
|
||||
/**
|
||||
* \brief Subscribe to changes of the named CVar (registered or not yet registered).
|
||||
*
|
||||
* Fired synchronously on the mutating thread (in practice the game thread) whenever the CVar's
|
||||
* effective value changes at runtime: setValue, override/speedrun setters and clears. Values
|
||||
* applied by config load or launch arguments do *not* notify: loads happen during startup
|
||||
* before the subsystems callbacks push values into are initialized, and each subsystem reads
|
||||
* its initial value itself at its own init. Callbacks may mutate other CVars; a nested
|
||||
* mutation of the same CVar applies but does not re-notify.
|
||||
*/
|
||||
Subscription subscribe(std::string_view name, ChangeCallback callback);
|
||||
|
||||
/**
|
||||
* \brief Typed convenience overload: the callback receives the current and previous values.
|
||||
*/
|
||||
template <ConfigValue T, typename Callback>
|
||||
requires std::invocable<Callback, const T&, const T&> Subscription subscribe(
|
||||
ConfigVar<T>& cVar, Callback&& callback) {
|
||||
return subscribe(cVar.getName(),
|
||||
[&cVar, cb = std::forward<Callback>(callback)](ConfigVarBase&, const void* previousValue) {
|
||||
cb(cVar.getValue(), *static_cast<const T*>(previousValue));
|
||||
});
|
||||
}
|
||||
|
||||
void unsubscribe(Subscription token);
|
||||
|
||||
/**
|
||||
* \brief Register a CVar and attach a change callback in one step.
|
||||
*
|
||||
* Useful for pushing settings into external systems (e.g. aurora) from one place instead of
|
||||
* every UI setter. The callback fires only for runtime changes (see subscribe); not when
|
||||
* loaded from config or launch arguments.
|
||||
*/
|
||||
template <ConfigValue T, typename Callback>
|
||||
requires std::invocable<Callback, const T&, const T&> Subscription Register(
|
||||
ConfigVar<T>& cVar, Callback&& onChange) {
|
||||
auto subscription = subscribe(cVar, std::forward<Callback>(onChange));
|
||||
Register(static_cast<ConfigVarBase&>(cVar));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
const ConfigImplBase* GetConfigImpl() {
|
||||
static ConfigImpl<T> config;
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
#define DUSK_CONFIG_VAR_HPP
|
||||
|
||||
#include "dolphin/types.h"
|
||||
#include <type_traits>
|
||||
#include <concepts>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* The configuration system.
|
||||
@@ -69,7 +71,7 @@ protected:
|
||||
/**
|
||||
* The name of this CVar, used in the configuration file.
|
||||
*/
|
||||
const char* name;
|
||||
std::string name;
|
||||
|
||||
/**
|
||||
* Whether this CVar has been registered with the global managing logic.
|
||||
@@ -87,8 +89,8 @@ protected:
|
||||
*/
|
||||
const ConfigImplBase* impl;
|
||||
|
||||
ConfigVarBase(const char* name, const ConfigImplBase* impl);
|
||||
virtual ~ConfigVarBase() = default;
|
||||
ConfigVarBase(const ConfigVarBase&) = delete;
|
||||
ConfigVarBase(std::string name, const ConfigImplBase* impl);
|
||||
|
||||
/**
|
||||
* Check that the CVar is registered, aborting if this is not the case.
|
||||
@@ -98,7 +100,22 @@ protected:
|
||||
abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any change subscriber (see config::subscribe) is attached to this CVar's name.
|
||||
*/
|
||||
[[nodiscard]] bool has_subscribers() const;
|
||||
|
||||
/**
|
||||
* Notify change subscribers (see config::subscribe) that the effective value of this CVar
|
||||
* changed. Called by mutators after the change has been applied; previousValue points at
|
||||
* the old value (a `const T*` for a `ConfigVar<T>`), valid only for the duration of the
|
||||
* call.
|
||||
*/
|
||||
void notify_changed(const void* previousValue);
|
||||
|
||||
public:
|
||||
virtual ~ConfigVarBase();
|
||||
|
||||
/**
|
||||
* Get the name of this CVar, used in the configuration file.
|
||||
*/
|
||||
@@ -121,6 +138,7 @@ public:
|
||||
* This is necessary to make it legal to access.
|
||||
*/
|
||||
void markRegistered();
|
||||
void unmarkRegistered();
|
||||
|
||||
/**
|
||||
* Clear a speedrun-mode override if one is active on this CVar.
|
||||
@@ -155,6 +173,7 @@ template <typename T>
|
||||
concept ConfigValue =
|
||||
!std::is_const_v<T>
|
||||
&& !std::is_volatile_v<T>
|
||||
&& std::equality_comparable<T>
|
||||
&& (std::is_same_v<T, bool>
|
||||
|| ConfigValueInteger<T>
|
||||
|| std::is_same_v<T, f32>
|
||||
@@ -166,6 +185,9 @@ concept ConfigValue =
|
||||
template <ConfigValue T>
|
||||
const ConfigImplBase* GetConfigImpl();
|
||||
|
||||
template <ConfigValue T>
|
||||
class ConfigImpl;
|
||||
|
||||
template <typename T>
|
||||
struct ConfigEnumRange {
|
||||
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
|
||||
@@ -192,10 +214,12 @@ public:
|
||||
* @param arg Arguments to forward to construct the default value.
|
||||
*/
|
||||
template <typename... Args>
|
||||
ConfigVar(const char* name, Args&&... arg)
|
||||
: ConfigVarBase(name, GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
|
||||
ConfigVar(std::string name, Args&&... arg)
|
||||
: ConfigVarBase(std::move(name), GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
|
||||
value(), overrideValue() {}
|
||||
|
||||
ConfigVar(ConfigVar const&) = delete;
|
||||
|
||||
/**
|
||||
* \brief Get the current value of the CVar.
|
||||
*
|
||||
@@ -234,6 +258,7 @@ public:
|
||||
*/
|
||||
void setValue(T newValue, bool replaceOverride = true) {
|
||||
checkRegistered();
|
||||
const auto previous = previous_for_notify();
|
||||
value = std::move(newValue);
|
||||
|
||||
if (replaceOverride) {
|
||||
@@ -242,6 +267,7 @@ public:
|
||||
} else if (layer != ConfigVarLayer::Override) {
|
||||
layer = ConfigVarLayer::Value;
|
||||
}
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
|
||||
operator const T&() {
|
||||
@@ -258,8 +284,10 @@ public:
|
||||
*/
|
||||
void setOverrideValue(T newValue) {
|
||||
checkRegistered();
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Override;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,25 +301,31 @@ public:
|
||||
void setSpeedrunValue(T newValue) {
|
||||
checkRegistered();
|
||||
if (layer != ConfigVarLayer::Override) {
|
||||
const auto previous = previous_for_notify();
|
||||
priorLayer = layer;
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Speedrun;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
void clearOverride() {
|
||||
checkRegistered();
|
||||
if (layer == ConfigVarLayer::Override) {
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = {};
|
||||
layer = ConfigVarLayer::Value;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
void clearSpeedrunOverride() override {
|
||||
checkRegistered();
|
||||
if (layer == ConfigVarLayer::Speedrun) {
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = {};
|
||||
layer = priorLayer;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +339,48 @@ public:
|
||||
const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer;
|
||||
return effectiveLayer == ConfigVarLayer::Default ? defaultValue : value;
|
||||
}
|
||||
|
||||
private:
|
||||
// The config loader applies values through the silent load_* methods below.
|
||||
friend class ConfigImpl<T>;
|
||||
|
||||
/**
|
||||
* Copy of the effective value before a mutation, taken only when someone is subscribed.
|
||||
*/
|
||||
[[nodiscard]] std::optional<T> previous_for_notify() const {
|
||||
return has_subscribers() ? std::optional<T>{getValue()} : std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify subscribers if the effective value actually changed across a mutation.
|
||||
*/
|
||||
void notify_if_changed(const std::optional<T>& previous) {
|
||||
if (previous.has_value() && !(getValue() == *previous)) {
|
||||
notify_changed(&*previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* setValue(newValue, false) without notifying change subscribers. Used when loading config:
|
||||
* loads happen during startup before the subsystems change callbacks push values into are
|
||||
* initialized, and each subsystem applies the loaded value itself at its own init.
|
||||
*/
|
||||
void load_value(T newValue) {
|
||||
checkRegistered();
|
||||
value = std::move(newValue);
|
||||
if (layer != ConfigVarLayer::Override) {
|
||||
layer = ConfigVarLayer::Value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* setOverrideValue without notifying change subscribers (see load_value).
|
||||
*/
|
||||
void load_override_value(T newValue) {
|
||||
checkRegistered();
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Override;
|
||||
}
|
||||
};
|
||||
|
||||
using ActionBindConfigVar = ConfigVar<int>;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/gfx.h"
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
void gfx_run_stage(GfxStage stage, const view_class* gameView = nullptr,
|
||||
const view_port_class* gameViewport = nullptr);
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <dolphin/gx/GXExtra.h>
|
||||
#include "tracy/Tracy.hpp"
|
||||
|
||||
#include "profiling.hpp"
|
||||
|
||||
#if DUSK_GFX_DEBUG_GROUPS
|
||||
#define GX_DEBUG_GROUP(name, ...) \
|
||||
|
||||
@@ -12,6 +12,18 @@ extern bool RestartRequested;
|
||||
extern std::filesystem::path ConfigPath;
|
||||
extern std::filesystem::path CachePath;
|
||||
|
||||
extern uint8_t SaveRequested;
|
||||
struct StageRequest {
|
||||
std::string stage;
|
||||
bool set;
|
||||
s8 room;
|
||||
s16 point;
|
||||
s8 layer;
|
||||
};
|
||||
extern StageRequest StageRequested;
|
||||
|
||||
|
||||
|
||||
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
|
||||
(defined(TARGET_OS_TV) && TARGET_OS_TV)
|
||||
inline constexpr bool SupportsProcessRestart = false;
|
||||
|
||||
@@ -6,6 +6,9 @@ class CPaneMgr;
|
||||
|
||||
namespace dusk::menu_pointer {
|
||||
|
||||
using TargetId = u16;
|
||||
constexpr TargetId InvalidTarget = 0xffff;
|
||||
|
||||
enum class Context {
|
||||
None,
|
||||
FileSelect,
|
||||
@@ -43,12 +46,14 @@ bool active() noexcept;
|
||||
bool enabled() noexcept;
|
||||
bool mouse_capture_active() noexcept;
|
||||
const State& state() noexcept;
|
||||
void set_hover_target(TargetId target) noexcept;
|
||||
bool consume_click() noexcept;
|
||||
bool peek_click() noexcept;
|
||||
void set_dialog_choice(u8 choice, bool clicked) noexcept;
|
||||
bool get_dialog_choice(u8& choice) noexcept;
|
||||
bool consume_dialog_click(u8& choice) noexcept;
|
||||
void defer_activation(Context context, u8 target) noexcept;
|
||||
bool consume_deferred_activation(Context context, u8 target) noexcept;
|
||||
void defer_activation(Context context, TargetId target) noexcept;
|
||||
bool consume_deferred_activation(Context context, TargetId target) noexcept;
|
||||
void clear_deferred_activation(Context context) noexcept;
|
||||
u32 suppressed_pad_buttons(u32 port) noexcept;
|
||||
void finish_pad_suppression_read(u32 port) noexcept;
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/config_var.hpp"
|
||||
#include "mods/api.h"
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
class ModBundle;
|
||||
} // namespace dusk::mods
|
||||
|
||||
struct ModContext {
|
||||
dusk::mods::LoadedMod* mod = nullptr;
|
||||
};
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
class NativeModule;
|
||||
} // namespace dusk::mods::loader
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
struct ModDependencyEdge {
|
||||
LoadedMod* mod = nullptr;
|
||||
bool required = false;
|
||||
};
|
||||
|
||||
struct ModManifestInfo {
|
||||
struct Import {
|
||||
std::string id;
|
||||
uint16_t major = 0;
|
||||
bool required = false;
|
||||
bool operator==(const Import&) const = default;
|
||||
};
|
||||
struct Export {
|
||||
std::string id;
|
||||
uint16_t major = 0;
|
||||
bool operator==(const Export&) const = default;
|
||||
};
|
||||
std::vector<Import> imports;
|
||||
std::vector<Export> exports;
|
||||
bool operator==(const ModManifestInfo&) const = default;
|
||||
};
|
||||
|
||||
struct ModMetadata {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::string author;
|
||||
std::string description;
|
||||
std::string iconPath;
|
||||
std::string bannerPath;
|
||||
};
|
||||
|
||||
struct ModSearchDir {
|
||||
std::filesystem::path path;
|
||||
// Directory bundles dlopen their native lib in place instead of extracting it to the cache.
|
||||
// Required where extracted code cannot run (iOS), desirable for signed/read-only installs.
|
||||
bool inPlaceNative = false;
|
||||
// Native library location for platforms that restrict placement (e.g. iOS/tvOS Frameworks/)
|
||||
std::filesystem::path nativeLibDir;
|
||||
};
|
||||
|
||||
struct NativeMod {
|
||||
std::unique_ptr<loader::NativeModule> handle;
|
||||
const ModManifest* manifest = nullptr;
|
||||
ModContext** contextSymbol = nullptr;
|
||||
|
||||
ModInitializeFn fn_initialize = nullptr;
|
||||
ModUpdateFn fn_update = nullptr;
|
||||
ModShutdownFn fn_shutdown = nullptr;
|
||||
};
|
||||
|
||||
enum class NativeModStatus : u8 {
|
||||
/**
|
||||
* Mod does not have native code included.
|
||||
*/
|
||||
None,
|
||||
|
||||
/**
|
||||
* Native code mod loaded successfully.
|
||||
*
|
||||
* Note that this only indicates load status of the native library. If the native lib throws in
|
||||
* its init function, it will still be disabled!
|
||||
*/
|
||||
Loaded,
|
||||
|
||||
/**
|
||||
* This build was compiled without native mod support!
|
||||
*/
|
||||
BuildDisabled,
|
||||
|
||||
/**
|
||||
* Mod ships native libraries, but none matches this build's platform and architecture.
|
||||
*/
|
||||
ModMissingPlatform,
|
||||
|
||||
/**
|
||||
* Mod is built for a different ABI version than this build of the game.
|
||||
*/
|
||||
ApiVersionMismatch,
|
||||
|
||||
/**
|
||||
* Mod is missing a required native API export.
|
||||
*/
|
||||
MissingExport,
|
||||
|
||||
/**
|
||||
* Unknown error loading the native mod.
|
||||
*/
|
||||
Unknown,
|
||||
};
|
||||
|
||||
struct LoadedMod {
|
||||
ModMetadata metadata;
|
||||
std::string modPath;
|
||||
std::string dir;
|
||||
|
||||
uint32_t searchDirIndex = 0;
|
||||
// Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported.
|
||||
bool inPlace = false;
|
||||
|
||||
std::unique_ptr<ConfigVar<bool> > cvarIsEnabled;
|
||||
config::Subscription enabledSubscription = 0;
|
||||
|
||||
bool active = false;
|
||||
bool loadFailed = false;
|
||||
std::string failureReason;
|
||||
|
||||
// mod_initialize succeeded; a mod_shutdown is owed on deactivation.
|
||||
bool initialized = false;
|
||||
// Static service exports are currently present in the registry.
|
||||
bool servicesRegistered = false;
|
||||
// Lifecycle state last applied by the loader; diffed against cvarIsEnabled to pick up
|
||||
// runtime enable/disable requests.
|
||||
bool enabledApplied = false;
|
||||
// Deactivated because a provider it imports from was disabled, not by its own cvar.
|
||||
bool suspendedByProvider = false;
|
||||
// Bumped per native lib extraction so every dlopen sees a fresh path (and thus a fresh
|
||||
// image with fresh statics; a previous dlclose may not fully unmap). Also bumped by
|
||||
// asset-only reloads, so it doubles as a generation for anything caching per-mod content.
|
||||
uint32_t cacheGeneration = 0;
|
||||
// Currently extracted native library, empty if none.
|
||||
std::string nativePath;
|
||||
|
||||
NativeModStatus nativeStatus = NativeModStatus::None;
|
||||
std::unique_ptr<NativeMod> native;
|
||||
std::unique_ptr<ModContext> context;
|
||||
|
||||
// Shared with overlay file registrations so in-flight DVD reads survive disable/reload.
|
||||
std::shared_ptr<ModBundle> bundle;
|
||||
|
||||
ModManifestInfo manifestInfo;
|
||||
|
||||
// Mods this mod imports services from, and mods importing services from this mod.
|
||||
std::vector<ModDependencyEdge> dependencies;
|
||||
std::vector<ModDependencyEdge> dependents;
|
||||
};
|
||||
|
||||
class ModLoader {
|
||||
public:
|
||||
static ModLoader& instance();
|
||||
|
||||
void set_search_dirs(std::vector<ModSearchDir> dirs) { m_searchDirs = std::move(dirs); }
|
||||
void set_cache_dir(std::filesystem::path dir) { m_cacheDir = std::move(dir); }
|
||||
void init();
|
||||
void tick();
|
||||
void shutdown();
|
||||
|
||||
void request_enable(std::string_view id);
|
||||
void request_disable(std::string_view id);
|
||||
void request_reload(std::string_view id);
|
||||
void notify_mod_failure(LoadedMod& mod, bool firstFailure);
|
||||
|
||||
[[nodiscard]] auto mods() const {
|
||||
return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; });
|
||||
}
|
||||
|
||||
[[nodiscard]] auto active_mods() const {
|
||||
return mods() | std::views::filter([](const auto& m) { return m.active; });
|
||||
}
|
||||
|
||||
private:
|
||||
enum class RequestKind : u8 { Enable, Disable, Reload };
|
||||
struct Request {
|
||||
std::string modId;
|
||||
RequestKind kind;
|
||||
};
|
||||
// ModLoader::tick runs inside fapGm_Execute, so code from an unloading mod can still be
|
||||
// live on the stack (its frame unwinds after the tick). dlclose is therefore deferred to
|
||||
// the next tick, by which point every per-frame entry into the mod should have returned.
|
||||
struct RetiredNative {
|
||||
std::unique_ptr<NativeMod> native;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<LoadedMod> > m_mods;
|
||||
std::vector<ModSearchDir> m_searchDirs;
|
||||
std::filesystem::path m_cacheDir;
|
||||
std::vector<Request> m_pendingRequests;
|
||||
std::vector<std::string> m_pendingFailures;
|
||||
std::vector<RetiredNative> m_retiredNatives;
|
||||
bool m_initialized = false;
|
||||
bool m_startupComplete = false;
|
||||
|
||||
void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex);
|
||||
void load_native(LoadedMod& mod, const std::string& dllEntry);
|
||||
// Resolved <nativeLibDir>/<mod id><ext> if it exists on disk, empty otherwise.
|
||||
[[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const;
|
||||
void unload_native(LoadedMod& mod);
|
||||
// Registers exports (if needed), resolves imports and runs mod_initialize.
|
||||
// Returns whether the mod ended up active; failures go through fail_mod.
|
||||
bool activate_mod(LoadedMod& mod);
|
||||
// Runs mod_shutdown (if needed), detaches the mod from every service, and unloads the
|
||||
// native lib. Must only run with no mod code on the stack (startup, shutdown, or top of tick).
|
||||
void deactivate_mod(LoadedMod& mod);
|
||||
void init_services();
|
||||
bool register_static_service_exports(LoadedMod& mod);
|
||||
bool resolve_service_imports(LoadedMod& mod);
|
||||
[[nodiscard]] std::string describe_missing_import(
|
||||
const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const;
|
||||
|
||||
LoadedMod* find_mod(std::string_view id) const;
|
||||
void drain_retired_natives();
|
||||
void apply_pending_requests();
|
||||
void flush_toasts();
|
||||
void on_enabled_changed(LoadedMod& mod);
|
||||
// Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the
|
||||
// bundle from disk, then reactivates whatever the current cvar/provider state allows.
|
||||
void apply_lifecycle_change(LoadedMod& target, bool reload);
|
||||
// `target` plus transitive active/suspended dependents, in m_mods (init) order.
|
||||
std::vector<LoadedMod*> collect_lifecycle_set(LoadedMod& target);
|
||||
bool reload_bundle(LoadedMod& mod);
|
||||
bool ensure_native_loaded(LoadedMod& mod);
|
||||
};
|
||||
|
||||
using ModIndex = std::ranges::range_difference_t<decltype(std::declval<ModLoader>().mods())>;
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
namespace dusk::mouse {
|
||||
void read();
|
||||
void getAimDeltas(float& out_yaw, float& out_pitch);
|
||||
void getCameraDeltas(float& out_yaw, float& out_pitch);
|
||||
void get_aim_deltas(float& out_yaw, float& out_pitch);
|
||||
void get_camera_deltas(float& out_yaw, float& out_pitch);
|
||||
void handle_event(const SDL_Event& event) noexcept;
|
||||
void onFocusLost();
|
||||
void onFocusGained();
|
||||
void on_focus_lost();
|
||||
void on_focus_gained();
|
||||
} // namespace dusk::mouse
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<tracy/Tracy.hpp>)
|
||||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef ZoneScoped
|
||||
#define ZoneScoped
|
||||
#define ZoneScopedN(name)
|
||||
#endif
|
||||
+15
-2
@@ -7,7 +7,8 @@
|
||||
|
||||
namespace dusk {
|
||||
|
||||
using namespace config;
|
||||
using config::ConfigVar;
|
||||
using config::ActionBindConfigVar;
|
||||
|
||||
enum class BloomMode : int {
|
||||
Off = 0,
|
||||
@@ -46,6 +47,12 @@ enum class FrameInterpMode : u8 {
|
||||
Unlimited = 2,
|
||||
};
|
||||
|
||||
enum class TouchTargeting : u8 {
|
||||
Hybrid = 0,
|
||||
Hold = 1,
|
||||
Switch = 2,
|
||||
};
|
||||
|
||||
enum class MenuScaling : u8 {
|
||||
GameCube = 0,
|
||||
Wii = 1,
|
||||
@@ -97,6 +104,12 @@ struct ConfigEnumRange<FrameInterpMode> {
|
||||
static constexpr auto max = FrameInterpMode::Unlimited;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ConfigEnumRange<TouchTargeting> {
|
||||
static constexpr auto min = TouchTargeting::Hybrid;
|
||||
static constexpr auto max = TouchTargeting::Switch;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ConfigEnumRange<MenuScaling> {
|
||||
static constexpr auto min = MenuScaling::GameCube;
|
||||
@@ -216,6 +229,7 @@ struct UserSettings {
|
||||
ConfigVar<bool> invertMouseY;
|
||||
ConfigVar<bool> freeCamera;
|
||||
ConfigVar<bool> enableTouchControls;
|
||||
ConfigVar<TouchTargeting> touchTargeting;
|
||||
ConfigVar<bool> enableMenuPointer;
|
||||
ConfigVar<ui::ControlLayout> touchControlsLayout;
|
||||
ConfigVar<bool> invertCameraXAxis;
|
||||
@@ -275,7 +289,6 @@ struct UserSettings {
|
||||
ConfigVar<DiscVerificationState> isoVerification;
|
||||
ConfigVar<std::string> graphicsBackend;
|
||||
ConfigVar<bool> skipPreLaunchUI;
|
||||
ConfigVar<bool> showPipelineCompilation;
|
||||
ConfigVar<bool> wasPresetChosen;
|
||||
ConfigVar<bool> checkForUpdates;
|
||||
ConfigVar<int> cardFileType;
|
||||
|
||||
@@ -37,5 +37,6 @@ struct SpeedrunInfo {
|
||||
extern SpeedrunInfo m_speedrunInfo;
|
||||
|
||||
void resetForSpeedrunMode();
|
||||
void restoreFromSpeedrunMode();
|
||||
|
||||
} // namespace dusk
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
#ifndef DUSK_TEXTURE_REPLACEMENTS_HPP
|
||||
#define DUSK_TEXTURE_REPLACEMENTS_HPP
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace dusk::texture_replacements {
|
||||
|
||||
// Mod replacements are prioritized *over* user replacements (<data folder>/texture_replacements/)
|
||||
inline constexpr int32_t kUserTextureReplacementPriority = -1'000'000;
|
||||
|
||||
void reload();
|
||||
void set_enabled(bool enabled);
|
||||
void shutdown();
|
||||
|
||||
@@ -36,7 +36,7 @@ typedef struct node_create_request {
|
||||
/* 0x58 */ s16 name;
|
||||
/* 0x5C */ void* data;
|
||||
/* 0x60 */ s16 unk_0x60;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x64 */ int unk_0x64;
|
||||
/* 0x68 */ int unk_0x68;
|
||||
#endif
|
||||
|
||||
@@ -114,6 +114,19 @@ inline int __builtin_clz(unsigned int v) {
|
||||
|
||||
#endif
|
||||
|
||||
// Data symbols exported from the main exe need dllimport on the mod side.
|
||||
// DUSK_BUILDING_GAME is defined for the game build so the same headers work in both.
|
||||
#if defined(TARGET_PC) && defined(_WIN32) && !defined(DUSK_BUILDING_GAME)
|
||||
#define DUSK_GAME_EXTERN extern __declspec(dllimport)
|
||||
#define DUSK_GAME_DATA __declspec(dllimport)
|
||||
#elif defined(TARGET_PC) && defined(_WIN32) && defined(DUSK_BUILDING_GAME)
|
||||
#define DUSK_GAME_EXTERN extern __declspec(dllexport)
|
||||
#define DUSK_GAME_DATA __declspec(dllexport)
|
||||
#else
|
||||
#define DUSK_GAME_EXTERN extern
|
||||
#define DUSK_GAME_DATA
|
||||
#endif
|
||||
|
||||
#define FAST_DIV(x, n) (x >> (n / 2))
|
||||
|
||||
#define SQUARE(x) ((x) * (x))
|
||||
|
||||
@@ -11,13 +11,13 @@ class mDoAud_zelAudio_c : public Z2AudioMgr {
|
||||
public:
|
||||
void reset();
|
||||
mDoAud_zelAudio_c() {
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
setMode(2);
|
||||
#endif
|
||||
}
|
||||
~mDoAud_zelAudio_c() {}
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
u8 getMode() { return field_0x13bd; }
|
||||
void setMode(u8 mode) { field_0x13bd = mode; }
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ public:
|
||||
/* 0x4 */ s8 mNo;
|
||||
/* 0x5 */ u8 mCount;
|
||||
#else
|
||||
#if PARTIAL_DEBUG
|
||||
// Initialized here since the DEBUG ctor doesn't run.
|
||||
/* 0x4 */ s8 mNo = -1;
|
||||
/* 0x5 */ u8 mCount = 0;
|
||||
#endif
|
||||
virtual ~mDoHIO_entry_c() {}
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
extern "C" {
|
||||
#else
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define MOD_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define MOD_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define MOD_EXTERN_C extern "C"
|
||||
#else
|
||||
#define MOD_EXTERN_C
|
||||
#endif
|
||||
|
||||
#define MOD_ABI_VERSION 5u
|
||||
#define MOD_ERROR_MESSAGE_SIZE 512u
|
||||
|
||||
typedef struct ModContext ModContext;
|
||||
|
||||
typedef enum ModResult {
|
||||
MOD_OK = 0,
|
||||
MOD_ERROR = 1,
|
||||
MOD_UNAVAILABLE = 2,
|
||||
MOD_UNSUPPORTED = 3,
|
||||
MOD_CONFLICT = 4,
|
||||
MOD_INVALID_ARGUMENT = 5,
|
||||
} ModResult;
|
||||
|
||||
static_assert(sizeof(ModResult) == 4, "mod SDK enums must be int-sized; do not build mods with -fshort-enums");
|
||||
|
||||
typedef struct ModError {
|
||||
uint32_t struct_size;
|
||||
ModResult code;
|
||||
char message[MOD_ERROR_MESSAGE_SIZE];
|
||||
} ModError;
|
||||
|
||||
#define MOD_ERROR_INIT {sizeof(ModError), MOD_OK, {0}}
|
||||
|
||||
/*
|
||||
* Opaque per-mod context, populated by the host before mod_initialize is called.
|
||||
* Pass it as the first argument to every service call; it identifies the calling
|
||||
* mod for attribution (logging, resource lookup, hook ownership, etc.).
|
||||
*/
|
||||
MOD_EXPORT extern ModContext* mod_ctx;
|
||||
|
||||
/*
|
||||
* Service versioning contract:
|
||||
*
|
||||
* A service is a struct of function pointers, beginning with a ServiceHeader.
|
||||
* Compatibility is tracked with a major/minor version pair:
|
||||
*
|
||||
* - A major version bump is a breaking change. Different majors are distinct
|
||||
* services; the registry never matches an import against a different major.
|
||||
* - A minor version bump may only append fields to the end of the struct.
|
||||
* Existing fields must keep their offsets and semantics.
|
||||
*
|
||||
* Providers: exporting minor N means every function pointer introduced at or
|
||||
* below N is populated (non-NULL). struct_size reflects the compiled struct.
|
||||
*
|
||||
* Importers: importing with min_minor_version N guarantees (enforced at load
|
||||
* time) that the resolved service is at least minor N, so any field introduced
|
||||
* at or below N may be used unconditionally, with no availability checks.
|
||||
* Fields newer than the declared min_minor_version must be gated behind
|
||||
* SERVICE_HAS plus a NULL check on the pointer itself.
|
||||
*
|
||||
* Load ordering: a manifest import of another mod's service (required or
|
||||
* optional) guarantees that the provider's mod_initialize completed before the
|
||||
* importer's runs, and deferred services published during the provider's
|
||||
* initialization resolve into import slots just like static exports. If a
|
||||
* provider fails to load, mods that required its services fail in turn. Mods
|
||||
* whose required imports form a cycle all fail to load; a cycle involving an
|
||||
* optional import is broken by dropping the ordering guarantee (not the
|
||||
* resolution) of that optional import. Dynamic lookups via
|
||||
* HostService::get_service carry no ordering guarantee: they see whatever has
|
||||
* been published at call time.
|
||||
*/
|
||||
typedef struct ServiceHeader {
|
||||
uint32_t struct_size;
|
||||
uint16_t major_version;
|
||||
uint16_t minor_version;
|
||||
} ServiceHeader;
|
||||
|
||||
#define SERVICE_HEADER(service_type, major, minor) {sizeof(service_type), (major), (minor)}
|
||||
|
||||
#define SERVICE_HAS(service, service_type, field) \
|
||||
((service) != NULL && \
|
||||
(service)->header.struct_size >= \
|
||||
(uint32_t)(offsetof(service_type, field) + sizeof(((service_type*)0)->field)))
|
||||
|
||||
typedef enum ServiceImportFlags {
|
||||
SERVICE_IMPORT_REQUIRED = 0u,
|
||||
SERVICE_IMPORT_OPTIONAL = 1u << 0u,
|
||||
} ServiceImportFlags;
|
||||
|
||||
typedef enum ServiceExportFlags {
|
||||
SERVICE_EXPORT_STATIC = 0u,
|
||||
SERVICE_EXPORT_DEFERRED = 1u << 0u,
|
||||
} ServiceExportFlags;
|
||||
|
||||
typedef struct ServiceImport {
|
||||
uint32_t struct_size;
|
||||
const char* service_id;
|
||||
uint16_t major_version;
|
||||
uint16_t min_minor_version;
|
||||
uint32_t flags;
|
||||
void* slot;
|
||||
} ServiceImport;
|
||||
|
||||
typedef struct ServiceExport {
|
||||
uint32_t struct_size;
|
||||
const char* service_id;
|
||||
uint16_t major_version;
|
||||
uint16_t minor_version;
|
||||
uint32_t flags;
|
||||
const void* service;
|
||||
} ServiceExport;
|
||||
|
||||
typedef struct ModManifest {
|
||||
uint32_t struct_size;
|
||||
uint32_t abi_version;
|
||||
const ServiceImport* imports;
|
||||
size_t import_count;
|
||||
const ServiceExport* exports;
|
||||
size_t export_count;
|
||||
} ModManifest;
|
||||
|
||||
typedef const ModManifest* (*ModGetManifestFn)(void);
|
||||
|
||||
typedef ModResult (*ModInitializeFn)(ModError* out_error);
|
||||
typedef ModResult (*ModUpdateFn)(ModError* out_error);
|
||||
typedef ModResult (*ModShutdownFn)(ModError* out_error);
|
||||
|
||||
MOD_EXPORT const ModManifest* mod_get_manifest(void);
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* out_error);
|
||||
MOD_EXPORT ModResult mod_update(ModError* out_error);
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError* out_error);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,464 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
template <class T>
|
||||
T arg(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T> > >(args[n]);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T> > >(args[n]);
|
||||
}
|
||||
|
||||
template <class F>
|
||||
void* mfp_addr(F fn) noexcept {
|
||||
void* p = nullptr;
|
||||
static_assert(sizeof(fn) >= sizeof(void*), "unexpected function pointer size");
|
||||
std::memcpy(&p, &fn, sizeof(void*));
|
||||
return p;
|
||||
}
|
||||
|
||||
/* A string usable as a template argument: carries the hook target's symbol name and
|
||||
* makes each NamedHook instantiation's static state unique. */
|
||||
template <size_t N>
|
||||
struct FixedString {
|
||||
char chars[N]{};
|
||||
constexpr FixedString(const char (&s)[N]) noexcept {
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
chars[i] = s[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <class T>
|
||||
constexpr std::string_view class_name() {
|
||||
#if defined(__clang__) || defined(__GNUC__)
|
||||
// "... class_name() [T = daAlink_c]" / "... [with T = daAlink_c; ...]"
|
||||
constexpr std::string_view fn = __PRETTY_FUNCTION__;
|
||||
constexpr size_t start = fn.find("T = ") + 4;
|
||||
return fn.substr(start, fn.find_first_of(";]", start) - start);
|
||||
#elif defined(_MSC_VER)
|
||||
// "... class_name<class daAlink_c>(void)"
|
||||
constexpr std::string_view fn = __FUNCSIG__;
|
||||
constexpr size_t start = fn.find("class_name<") + 11;
|
||||
constexpr std::string_view name = fn.substr(start, fn.rfind(">(") - start);
|
||||
if constexpr (name.starts_with("class ")) {
|
||||
return name.substr(6);
|
||||
} else if constexpr (name.starts_with("struct ")) {
|
||||
return name.substr(7);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
#else
|
||||
#error "unsupported compiler"
|
||||
#endif
|
||||
}
|
||||
|
||||
/* The manifest name of C's vtable. Only unscoped, non-template class names are
|
||||
* supported (an empty result fails resolution and the install reports it). */
|
||||
template <class C>
|
||||
constexpr auto vtable_symbol() {
|
||||
constexpr std::string_view name = class_name<C>();
|
||||
constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos;
|
||||
// "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated
|
||||
std::array<char, name.size() + 12> out{};
|
||||
if constexpr (!simple) {
|
||||
return out;
|
||||
}
|
||||
size_t n = 0;
|
||||
#if defined(_WIN32)
|
||||
for (char c : {'?', '?', '_', '7'}) {
|
||||
out[n++] = c;
|
||||
}
|
||||
for (char c : name) {
|
||||
out[n++] = c;
|
||||
}
|
||||
for (char c : {'@', '@', '6', 'B', '@'}) {
|
||||
out[n++] = c;
|
||||
}
|
||||
#else
|
||||
for (char c : {'_', 'Z', 'T', 'V'}) {
|
||||
out[n++] = c;
|
||||
}
|
||||
size_t len = name.size();
|
||||
char digits[8]{};
|
||||
size_t d = 0;
|
||||
while (len != 0) {
|
||||
digits[d++] = static_cast<char>('0' + len % 10);
|
||||
len /= 10;
|
||||
}
|
||||
while (d != 0) {
|
||||
out[n++] = digits[--d];
|
||||
}
|
||||
for (char c : name) {
|
||||
out[n++] = c;
|
||||
}
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at.
|
||||
* Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */
|
||||
inline size_t vcall_slot_offset(const void*& fn) noexcept {
|
||||
constexpr size_t npos = static_cast<size_t>(-1);
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
const auto* p = static_cast<const uint8_t*>(fn);
|
||||
for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs
|
||||
int32_t rel;
|
||||
std::memcpy(&rel, p + 1, 4);
|
||||
p += 5 + rel;
|
||||
}
|
||||
fn = p;
|
||||
// The vptr load. Unoptimized clang-cl thunks spill/reload rcx first
|
||||
// (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window.
|
||||
const uint8_t* q = nullptr;
|
||||
for (int i = 0; i <= 12; ++i) {
|
||||
if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx]
|
||||
q = p + i + 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (q == nullptr) {
|
||||
return npos;
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC)
|
||||
return 0;
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8]
|
||||
return static_cast<int8_t>(q[2]);
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32]
|
||||
int32_t off;
|
||||
std::memcpy(&off, q + 2, 4);
|
||||
return off;
|
||||
}
|
||||
// clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax
|
||||
// distinguishes the thunk from an ordinary getter that begins the same way.
|
||||
if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) {
|
||||
size_t off = 0;
|
||||
const uint8_t* r = q + 3;
|
||||
if (q[2] == 0x40) {
|
||||
off = static_cast<int8_t>(q[3]);
|
||||
r = q + 4;
|
||||
} else if (q[2] == 0x80) {
|
||||
int32_t off32;
|
||||
std::memcpy(&off32, q + 3, 4);
|
||||
off = off32;
|
||||
r = q + 7;
|
||||
}
|
||||
for (int i = 0; i <= 8; ++i) {
|
||||
if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional)
|
||||
return off;
|
||||
}
|
||||
}
|
||||
}
|
||||
return npos;
|
||||
#elif defined(_M_ARM64) || defined(__aarch64__)
|
||||
const auto* p = static_cast<const uint8_t*>(fn);
|
||||
uint32_t insn[3];
|
||||
for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs
|
||||
std::memcpy(insn, p, 4);
|
||||
if ((insn[0] & 0xFC000000u) != 0x14000000u) {
|
||||
break;
|
||||
}
|
||||
const auto imm26 = static_cast<int32_t>(insn[0] << 6) >> 6;
|
||||
p += static_cast<intptr_t>(imm26) * 4;
|
||||
}
|
||||
fn = p;
|
||||
std::memcpy(insn, p, 12);
|
||||
// ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs
|
||||
if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) {
|
||||
return npos;
|
||||
}
|
||||
const uint32_t t = insn[0] & 0x1Fu;
|
||||
if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) {
|
||||
return npos;
|
||||
}
|
||||
const uint32_t s = insn[1] & 0x1Fu;
|
||||
if (insn[2] != (0xD61F0000u | (s << 5))) {
|
||||
return npos;
|
||||
}
|
||||
return ((insn[1] >> 10) & 0xFFFu) * 8;
|
||||
#else
|
||||
(void)fn;
|
||||
return npos;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Code address of the member function a mfp designates. Virtual mfps don't carry
|
||||
* one; recover it from the class's vtable (resolved from the symbol manifest), so
|
||||
* Hook works uniformly on virtual and non-virtual members. */
|
||||
template <class C, class F>
|
||||
ModResult member_target(const HookService* hooks, F mfp, void** out) {
|
||||
*out = nullptr;
|
||||
uintptr_t words[sizeof(F) > sizeof(uintptr_t) ? 2 : 1] = {};
|
||||
std::memcpy(words, &mfp, sizeof(words) < sizeof(F) ? sizeof(words) : sizeof(F));
|
||||
|
||||
#if defined(_WIN32)
|
||||
const void* fn = reinterpret_cast<const void*>(words[0]);
|
||||
const size_t slot = vcall_slot_offset(fn);
|
||||
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
|
||||
*out = const_cast<void*>(fn);
|
||||
return MOD_OK;
|
||||
}
|
||||
void* vtable = nullptr;
|
||||
const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol<C>().data(), &vtable, nullptr);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
}
|
||||
// ??_7 points at the first slot.
|
||||
*out = *reinterpret_cast<void**>(static_cast<char*>(vtable) + slot);
|
||||
#else
|
||||
#if defined(__aarch64__) || defined(__arm__)
|
||||
// AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function
|
||||
// addresses can't spare their low bit), and ptr holds the slot offset directly.
|
||||
const bool isVirtual = (words[1] & 1) != 0;
|
||||
const uintptr_t thisAdjust = words[1] >> 1;
|
||||
const uintptr_t slotOffset = words[0];
|
||||
#else
|
||||
// Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1.
|
||||
const bool isVirtual = (words[0] & 1) != 0;
|
||||
const uintptr_t thisAdjust = words[1];
|
||||
const uintptr_t slotOffset = words[0] - 1;
|
||||
#endif
|
||||
if (!isVirtual) { // non-virtual: the address itself
|
||||
*out = reinterpret_cast<void*>(words[0]);
|
||||
return MOD_OK;
|
||||
}
|
||||
if (thisAdjust != 0) {
|
||||
// this-adjusting mfp (member of a secondary base): the slot offset is
|
||||
// relative to a vtable we can't locate. Hook the overrider by name instead.
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
void* vtable = nullptr;
|
||||
const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol<C>().data(), &vtable, nullptr);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
}
|
||||
// _ZTV points at the offset-to-top slot; the address point mfps index from is
|
||||
// two pointers in (past offset-to-top and the typeinfo pointer).
|
||||
*out = *reinterpret_cast<void**>(static_cast<char*>(vtable) + 2 * sizeof(void*) + slotOffset);
|
||||
#endif
|
||||
return *out != nullptr ? MOD_OK : MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/* Trampoline generator + per-target state shared by Hook and NamedHook. Tag makes
|
||||
* each hooked target's statics distinct; the target address is filled in at install. */
|
||||
template <class Tag, class R, class... A>
|
||||
struct HookImpl {
|
||||
static inline R (*g_orig)(A...) = nullptr;
|
||||
static inline const HookService* hooks = nullptr;
|
||||
static inline void* target = nullptr;
|
||||
|
||||
static bool dispatch_pre(void* args, void* retval) {
|
||||
if (hooks == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int skipOriginal = 0;
|
||||
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
|
||||
return result == MOD_OK && skipOriginal != 0;
|
||||
}
|
||||
|
||||
static void dispatch_post(void* args, void* retval) {
|
||||
if (hooks != nullptr) {
|
||||
hooks->dispatch_post(mod_ctx, target, args, retval);
|
||||
}
|
||||
}
|
||||
|
||||
static R trampoline(A... args) {
|
||||
if constexpr (sizeof...(A) == 0) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal =
|
||||
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal = dispatch_pre(
|
||||
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <auto Target>
|
||||
using TargetTag = std::integral_constant<decltype(Target), Target>;
|
||||
template <FixedString Name>
|
||||
struct NameTag {};
|
||||
} // namespace detail
|
||||
|
||||
/*
|
||||
* Typed hook on a function named at compile time (&daAlink_c::execute, &free_fn).
|
||||
* Member functions may be virtual: the install decodes the member function pointer and hooks the
|
||||
* class's own overrider.
|
||||
*/
|
||||
template <auto Target>
|
||||
struct Hook;
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {
|
||||
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||
return detail::member_target<C>(hooks, Target, out);
|
||||
}
|
||||
};
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...) const>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {
|
||||
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||
return detail::member_target<C>(hooks, Target, out);
|
||||
}
|
||||
};
|
||||
|
||||
template <class R, class... A, R (*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {
|
||||
static ModResult resolve_target(const HookService*, void** out) {
|
||||
*out = mfp_addr(Target);
|
||||
return MOD_OK;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Typed hook on a function by its symbol name, for targets you can't name in C++: file-local
|
||||
* statics, private members, or symbols without a header. The signature is written free-style with
|
||||
* the receiver first and is *not* compiler-checked.
|
||||
*
|
||||
* using HookshotHit = dusk::mods::NamedHook<
|
||||
* "daAlink_hookshotAtHitCallBack",
|
||||
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>;
|
||||
* dusk::mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit);
|
||||
*/
|
||||
template <FixedString Name, class Sig>
|
||||
struct NamedHook;
|
||||
|
||||
template <FixedString Name, class R, class... A>
|
||||
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, R, A...> {
|
||||
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||
HookSymbolFlags flags{};
|
||||
const ModResult resolved = hooks->resolve(mod_ctx, Name.chars, out, &flags);
|
||||
if (resolved == MOD_OK && (flags & HOOK_SYMBOL_CODE) == 0) {
|
||||
*out = nullptr;
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
};
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
if (hooks == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
Entry::hooks = hooks;
|
||||
if (Entry::target == nullptr) {
|
||||
const ModResult resolved = Entry::resolve_target(hooks, &Entry::target);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
|
||||
reinterpret_cast<void**>(&Entry::g_orig));
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
return hook_install<Hook<Target> >(hooks);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_add_pre<Hook<Target> >(hooks, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
return hooks->add_post(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_add_post<Hook<Target> >(hooks, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
return hooks->replace(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_replace<Hook<Target> >(hooks, callback, options);
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
template <class Service>
|
||||
struct ServiceTraits;
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline std::vector<ServiceImport>& imports() {
|
||||
static std::vector<ServiceImport> entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
inline std::vector<ServiceExport>& exports() {
|
||||
static std::vector<ServiceExport> entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
inline int register_import(ServiceImport entry) {
|
||||
imports().push_back(entry);
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline int register_export(ServiceExport entry) {
|
||||
exports().push_back(entry);
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline const ModManifest* manifest() {
|
||||
static ModManifest manifest{
|
||||
sizeof(ModManifest),
|
||||
MOD_ABI_VERSION,
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
0,
|
||||
};
|
||||
|
||||
auto& importEntries = imports();
|
||||
auto& exportEntries = exports();
|
||||
manifest.imports = importEntries.data();
|
||||
manifest.import_count = importEntries.size();
|
||||
manifest.exports = exportEntries.data();
|
||||
manifest.export_count = exportEntries.size();
|
||||
return &manifest;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline ModResult set_error(ModError* outError, ModResult code, const char* message) {
|
||||
if (outError != nullptr && outError->struct_size >= sizeof(ModError)) {
|
||||
outError->code = code;
|
||||
outError->message[0] = '\0';
|
||||
if (message != nullptr) {
|
||||
std::snprintf(outError->message, sizeof(outError->message), "%s", message);
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
|
||||
#define DEFINE_MOD() \
|
||||
extern "C" { \
|
||||
MOD_EXPORT ModContext* mod_ctx = nullptr; \
|
||||
MOD_EXPORT const ModManifest* mod_get_manifest(void) { \
|
||||
return ::dusk::mods::detail::manifest(); \
|
||||
} \
|
||||
}
|
||||
|
||||
// Declares `static const service_type* variable`, filled in by the host before mod_initialize.
|
||||
// Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports
|
||||
// must be checked against nullptr before use.
|
||||
#define IMPORT_SERVICE_EX( \
|
||||
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
|
||||
static const service_type* variable = nullptr; \
|
||||
[[maybe_unused]] static const int mod_import_registration_##variable = \
|
||||
::dusk::mods::detail::register_import(ServiceImport{ \
|
||||
sizeof(ServiceImport), \
|
||||
(service_id_value), \
|
||||
static_cast<uint16_t>(major_value), \
|
||||
static_cast<uint16_t>(min_minor_value), \
|
||||
static_cast<uint32_t>(flags_value), \
|
||||
&(variable), \
|
||||
})
|
||||
|
||||
#define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \
|
||||
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
|
||||
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
|
||||
SERVICE_IMPORT_REQUIRED)
|
||||
|
||||
#define IMPORT_SERVICE(service_type, variable) IMPORT_SERVICE_VERSION(service_type, variable, 0)
|
||||
|
||||
#define IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, min_minor_value) \
|
||||
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
|
||||
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
|
||||
SERVICE_IMPORT_OPTIONAL)
|
||||
|
||||
#define IMPORT_OPTIONAL_SERVICE(service_type, variable) \
|
||||
IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, 0)
|
||||
|
||||
#define EXPORT_SERVICE_AS(instance, service_id_value) \
|
||||
namespace { \
|
||||
const int mod_export_registration_##instance = \
|
||||
::dusk::mods::detail::register_export(ServiceExport{ \
|
||||
sizeof(ServiceExport), \
|
||||
(service_id_value), \
|
||||
(instance).header.major_version, \
|
||||
(instance).header.minor_version, \
|
||||
SERVICE_EXPORT_STATIC, \
|
||||
&(instance), \
|
||||
}); \
|
||||
}
|
||||
|
||||
#define EXPORT_SERVICE(instance) \
|
||||
EXPORT_SERVICE_AS( \
|
||||
instance, ::dusk::mods::ServiceTraits<std::remove_cv_t<decltype(instance)> >::id)
|
||||
|
||||
#define EXPORT_DEFERRED_SERVICE(token, service_id_value, major_value, minor_value) \
|
||||
namespace { \
|
||||
const int mod_deferred_export_registration_##token = \
|
||||
::dusk::mods::detail::register_export(ServiceExport{ \
|
||||
sizeof(ServiceExport), \
|
||||
(service_id_value), \
|
||||
static_cast<uint16_t>(major_value), \
|
||||
static_cast<uint16_t>(minor_value), \
|
||||
SERVICE_EXPORT_DEFERRED, \
|
||||
nullptr, \
|
||||
}); \
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera"
|
||||
#define CAMERA_SERVICE_MAJOR 1u
|
||||
#define CAMERA_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* Snapshot of a game camera for the frame currently being recorded.
|
||||
*
|
||||
* Matrix conventions: every matrix is a column-major float[16] using the matrix * column-vector
|
||||
* convention, ready to memcpy into a WGSL mat4x4f uniform. NOTE: this is the TRANSPOSE of the
|
||||
* game's row-major Mtx/Mtx44 layout; mods that want the raw game matrices should read the
|
||||
* view_class directly instead.
|
||||
*
|
||||
* View space is right-handed with -Z forward. Projection matrices are in WebGPU clip convention
|
||||
* and follow the renderer's depth mode: reversed-Z by default (depth 1.0 at the near plane,
|
||||
* 0.0 at far).
|
||||
*
|
||||
* Unprojecting a depth-buffer texel at uv with sampled depth d:
|
||||
* let ndc = vec3f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, d); // WebGPU framebuffer y is down
|
||||
* let world4 = world_from_proj * vec4f(ndc, 1.0);
|
||||
* let world = world4.xyz / world4.w;
|
||||
*/
|
||||
typedef struct CameraInfo {
|
||||
uint32_t struct_size;
|
||||
|
||||
float view_from_world[16]; /* the view matrix */
|
||||
float world_from_view[16]; /* its inverse; column 3 is the camera position */
|
||||
float proj_from_view[16]; /* WebGPU-convention projection (+ Aurora reversed-Z) */
|
||||
float view_from_proj[16]; /* its inverse */
|
||||
float proj_from_world[16]; /* proj_from_view * view_from_world */
|
||||
float world_from_proj[16]; /* one-step depth-buffer -> world unproject */
|
||||
|
||||
float eye[3]; /* camera position in world space */
|
||||
float fovy; /* vertical field of view, degrees */
|
||||
float aspect;
|
||||
float near_plane;
|
||||
float far_plane;
|
||||
} CameraInfo;
|
||||
|
||||
#define CAMERA_INFO_INIT {sizeof(CameraInfo)}
|
||||
|
||||
typedef struct CameraService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Snapshots a camera. game_view must be a view_class pointer, such as from a render stage
|
||||
* callback's game view. Game thread only. Returns MOD_UNAVAILABLE when the view is not a valid
|
||||
* perspective camera.
|
||||
*/
|
||||
ModResult (*get_camera)(ModContext* ctx, const void* game_view, CameraInfo* out_info);
|
||||
} CameraService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<CameraService> {
|
||||
static constexpr const char* id = CAMERA_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CAMERA_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config"
|
||||
#define CONFIG_SERVICE_MAJOR 1u
|
||||
#define CONFIG_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for a config var registered by the calling mod. 0 is never a valid handle. */
|
||||
typedef uint64_t ConfigVarHandle;
|
||||
/* Handle for a change subscription. 0 is never a valid handle. */
|
||||
typedef uint64_t ConfigSubscriptionHandle;
|
||||
|
||||
typedef enum ConfigVarType {
|
||||
CONFIG_VAR_BOOL = 0, /* bool */
|
||||
CONFIG_VAR_INT = 1, /* int64_t */
|
||||
CONFIG_VAR_FLOAT = 2, /* double */
|
||||
CONFIG_VAR_STRING = 3, /* UTF-8 */
|
||||
} ConfigVarType;
|
||||
|
||||
typedef struct ConfigVarDesc {
|
||||
uint32_t struct_size;
|
||||
/* Name fragment: 1-64 characters from [A-Za-z0-9_-]. The full config key is
|
||||
* "mod.<escaped mod id>.<name>", persisted in config.json alongside host settings.
|
||||
* "enabled" is reserved by the loader. */
|
||||
const char* name;
|
||||
ConfigVarType type;
|
||||
/* Default value; only the field matching `type` is read. */
|
||||
bool default_bool;
|
||||
int64_t default_int;
|
||||
double default_float;
|
||||
const char* default_string; /* NULL means "" */
|
||||
} ConfigVarDesc;
|
||||
|
||||
#define CONFIG_VAR_DESC_INIT {sizeof(ConfigVarDesc), NULL, CONFIG_VAR_BOOL, false, 0, 0.0, NULL}
|
||||
|
||||
/* Snapshot of a var's value; only the field matching `type` is meaningful. */
|
||||
typedef struct ConfigVarValue {
|
||||
uint32_t struct_size;
|
||||
ConfigVarType type;
|
||||
bool bool_value;
|
||||
int64_t int_value;
|
||||
double float_value;
|
||||
const char* string_value; /* NUL-terminated; NULL for non-string vars */
|
||||
size_t string_length; /* excludes the NUL */
|
||||
} ConfigVarValue;
|
||||
|
||||
/*
|
||||
* Fired on the game thread whenever the var's effective value changes at runtime: the calling mod's
|
||||
* own set_* calls and any other runtime writer. Writes that leave the value unchanged do not fire,
|
||||
* and neither do values applied from config.json or --cvar during registration. `value` holds the
|
||||
* new (current) value and `previous` the one it replaced; both snapshots are valid only for the
|
||||
* duration of the call (copy string_value if you need to keep it). Setting the same var from inside
|
||||
* its own callback applies the write but is not re-notified.
|
||||
*/
|
||||
typedef void (*ConfigChangedFn)(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value,
|
||||
const ConfigVarValue* previous, void* user_data);
|
||||
|
||||
/*
|
||||
* Scoped configuration variables.
|
||||
*
|
||||
* Registrations are owned by the calling mod and removed automatically (subscriptions included)
|
||||
* when it is disabled, reloaded, or fails. Values are saved to config.json. Writes are debounced,
|
||||
* not flushed per set.
|
||||
*/
|
||||
typedef struct ConfigService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Register a config var. If a value for the full key was saved earlier (or set via --cvar),
|
||||
* it takes effect immediately; otherwise the var starts at the default. Registering a name
|
||||
* that is already live is MOD_CONFLICT. */
|
||||
ModResult (*register_var)(
|
||||
ModContext* ctx, const ConfigVarDesc* desc, ConfigVarHandle* out_handle);
|
||||
/* Unregister a var previously registered by the calling mod. Its persisted value is kept. */
|
||||
ModResult (*unregister_var)(ModContext* ctx, ConfigVarHandle var);
|
||||
|
||||
/* Typed accessors; the type must match the registration (MOD_INVALID_ARGUMENT otherwise). */
|
||||
ModResult (*get_bool)(ModContext* ctx, ConfigVarHandle var, bool* out_value);
|
||||
ModResult (*set_bool)(ModContext* ctx, ConfigVarHandle var, bool value);
|
||||
ModResult (*get_int)(ModContext* ctx, ConfigVarHandle var, int64_t* out_value);
|
||||
ModResult (*set_int)(ModContext* ctx, ConfigVarHandle var, int64_t value);
|
||||
ModResult (*get_float)(ModContext* ctx, ConfigVarHandle var, double* out_value);
|
||||
ModResult (*set_float)(ModContext* ctx, ConfigVarHandle var, double value);
|
||||
/* Copies the NUL-terminated value into buffer. out_length (optional) receives the full
|
||||
* length excluding the NUL regardless of buffer size; call with buffer == NULL and
|
||||
* buffer_size == 0 to query the length. A non-NULL buffer that is too small fails with
|
||||
* MOD_INVALID_ARGUMENT and writes nothing. */
|
||||
ModResult (*get_string)(
|
||||
ModContext* ctx, ConfigVarHandle var, char* buffer, size_t buffer_size, size_t* out_length);
|
||||
ModResult (*set_string)(ModContext* ctx, ConfigVarHandle var, const char* value);
|
||||
|
||||
/* Subscribe to changes of a var registered by the calling mod. out_handle may be NULL if
|
||||
* the subscription is never removed manually (cleanup on mod teardown is automatic). */
|
||||
ModResult (*subscribe)(ModContext* ctx, ConfigVarHandle var, ConfigChangedFn callback,
|
||||
void* user_data, ConfigSubscriptionHandle* out_handle);
|
||||
ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle);
|
||||
} ConfigService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<ConfigService> {
|
||||
static constexpr const char* id = CONFIG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* Mods that link or hook game code directly must import this service; service-only and asset-only
|
||||
* mods must not.
|
||||
*
|
||||
* Major version is the game-code ABI epoch: it is bumped when game-visible struct or vtable layouts
|
||||
* change incompatibly (e.g. a TARGET_PC field added to an existing game struct). The loader's
|
||||
* ordinary version check then fails mods built against the old epoch with a clear message instead
|
||||
* of letting them corrupt memory.
|
||||
*/
|
||||
#define GAME_SERVICE_ID "dev.twilitrealm.dusklight.game"
|
||||
#define GAME_SERVICE_MAJOR 1u
|
||||
#define GAME_SERVICE_MINOR 0u
|
||||
|
||||
typedef struct GameService {
|
||||
ServiceHeader header;
|
||||
} GameService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<GameService> {
|
||||
static constexpr const char* id = GAME_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
/*
|
||||
* Direct WebGPU access at various stages of the rendering pipeline. Mods use the wgpu* C API
|
||||
* (via webgpu/webgpu.h) for custom draws and compute dispatches.
|
||||
*
|
||||
* Every service function must be called on the game thread. GfxStageFn callbacks run on the game
|
||||
* thread during frame recording. push_draw, push_* and pass functions are valid from a stage
|
||||
* callback and anywhere else GX commands are being recorded.
|
||||
*
|
||||
* GfxDrawFn and GfxComputeFn callbacks run on the render worker thread while the frame is encoded.
|
||||
* They may use only the handles in their context struct and raw wgpu* calls; no other service may
|
||||
* be called from them.
|
||||
*
|
||||
* All WGPU handles provided by this service are borrowed. Handles in callback contexts are valid
|
||||
* only for the duration of the callback; views in GfxResolvedTargets are valid for the current
|
||||
* frame only. GPU objects a mod creates through raw wgpu calls are its own responsibility and
|
||||
* should be released in mod_shutdown. The device outlives all mods.
|
||||
*/
|
||||
|
||||
#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx"
|
||||
#define GFX_SERVICE_MAJOR 1u
|
||||
#define GFX_SERVICE_MINOR 0u
|
||||
|
||||
/* Maximum size for push_draw payload */
|
||||
#define GFX_INLINE_DRAW_PAYLOAD_SIZE 128u
|
||||
|
||||
/* 0 is never a valid handle. */
|
||||
typedef uint64_t GfxDrawTypeHandle;
|
||||
typedef uint64_t GfxStageHookHandle;
|
||||
typedef uint64_t GfxComputeTypeHandle;
|
||||
|
||||
/* A suballocation in one of the shared per-frame streaming buffers. */
|
||||
typedef struct GfxRange {
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
} GfxRange;
|
||||
|
||||
/*
|
||||
* Device and scene pass configuration. Valid from mod_initialize onward and stable for the
|
||||
* session. Offscreen passes from create_pass are always single-sample.
|
||||
*/
|
||||
typedef struct GfxDeviceInfo {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device; /* borrowed */
|
||||
WGPUQueue queue; /* borrowed */
|
||||
WGPUTextureFormat color_format; /* scene color target format */
|
||||
WGPUTextureFormat depth_format; /* scene depth target format */
|
||||
uint32_t sample_count; /* scene pass MSAA sample count */
|
||||
bool uses_reversed_z; /* true means depth 1.0 is near */
|
||||
} GfxDeviceInfo;
|
||||
|
||||
#define GFX_DEVICE_INFO_INIT \
|
||||
{sizeof(GfxDeviceInfo), NULL, NULL, WGPUTextureFormat_Undefined, WGPUTextureFormat_Undefined, \
|
||||
1u, false}
|
||||
|
||||
/*
|
||||
* Passed to GfxDrawFn on the render worker thread; valid only during the call. The pass pipeline,
|
||||
* bind group, viewport, and scissor state is restored by the host after the callback returns.
|
||||
*/
|
||||
typedef struct GfxDrawContext {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device;
|
||||
WGPUQueue queue;
|
||||
WGPURenderPassEncoder pass;
|
||||
WGPUBuffer vertex_buffer;
|
||||
WGPUBuffer index_buffer;
|
||||
WGPUBuffer uniform_buffer;
|
||||
WGPUBuffer storage_buffer;
|
||||
WGPUTextureFormat color_format;
|
||||
WGPUTextureFormat depth_format;
|
||||
uint32_t sample_count;
|
||||
uint32_t target_width;
|
||||
uint32_t target_height;
|
||||
bool uses_reversed_z;
|
||||
} GfxDrawContext;
|
||||
|
||||
typedef void (*GfxDrawFn)(ModContext* ctx, const GfxDrawContext* draw_ctx, const void* payload,
|
||||
size_t payload_size, void* user_data);
|
||||
|
||||
typedef struct GfxDrawTypeDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* optional debug label */
|
||||
GfxDrawFn draw; /* required; called from the render worker thread */
|
||||
void* user_data;
|
||||
} GfxDrawTypeDesc;
|
||||
|
||||
#define GFX_DRAW_TYPE_DESC_INIT {sizeof(GfxDrawTypeDesc), NULL, NULL, NULL}
|
||||
|
||||
typedef enum GfxStage {
|
||||
GFX_STAGE_SCENE_AFTER_TERRAIN = 0,
|
||||
GFX_STAGE_FRAME_BEFORE_HUD = 1,
|
||||
GFX_STAGE_FRAME_AFTER_HUD = 2,
|
||||
GFX_STAGE_SCENE_BEGIN = 3,
|
||||
GFX_STAGE_SCENE_AFTER_OPAQUE = 4,
|
||||
} GfxStage;
|
||||
|
||||
typedef struct GfxStageContext {
|
||||
uint32_t struct_size;
|
||||
GfxStage stage;
|
||||
const void* game_view; /* view_class* for world-camera stages; NULL otherwise */
|
||||
const void* game_viewport; /* view_port_class* for world-camera stages; NULL otherwise */
|
||||
} GfxStageContext;
|
||||
|
||||
typedef void (*GfxStageFn)(ModContext* ctx, const GfxStageContext* stage_ctx, void* user_data);
|
||||
|
||||
typedef struct GfxStageHookDesc {
|
||||
uint32_t struct_size;
|
||||
GfxStageFn callback; /* required */
|
||||
void* user_data;
|
||||
} GfxStageHookDesc;
|
||||
|
||||
#define GFX_STAGE_HOOK_DESC_INIT {sizeof(GfxStageHookDesc), NULL, NULL}
|
||||
|
||||
typedef struct GfxResolveDesc {
|
||||
uint32_t struct_size;
|
||||
bool color;
|
||||
bool depth;
|
||||
} GfxResolveDesc;
|
||||
|
||||
#define GFX_RESOLVE_DESC_INIT {sizeof(GfxResolveDesc), true, false}
|
||||
|
||||
typedef struct GfxResolvedTargets {
|
||||
uint32_t struct_size;
|
||||
WGPUTextureView color; /* single-sample snapshot in color_format */
|
||||
WGPUTextureView depth; /* single-sample raw depth snapshot, R32Float when available */
|
||||
WGPUTextureFormat color_format;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} GfxResolvedTargets;
|
||||
|
||||
#define GFX_RESOLVED_TARGETS_INIT \
|
||||
{sizeof(GfxResolvedTargets), NULL, NULL, WGPUTextureFormat_Undefined, 0u, 0u}
|
||||
|
||||
/*
|
||||
* Passed to GfxComputeFn on the render worker thread; valid only during the call. The encoder is
|
||||
* the frame command encoder between scene render passes. Leave no pass open and never finish or
|
||||
* release the encoder.
|
||||
*/
|
||||
typedef struct GfxComputeContext {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device;
|
||||
WGPUQueue queue;
|
||||
WGPUCommandEncoder encoder;
|
||||
WGPUBuffer vertex_buffer;
|
||||
WGPUBuffer index_buffer;
|
||||
WGPUBuffer uniform_buffer;
|
||||
WGPUBuffer storage_buffer;
|
||||
} GfxComputeContext;
|
||||
|
||||
typedef void (*GfxComputeFn)(ModContext* ctx, const GfxComputeContext* compute_ctx,
|
||||
const void* payload, size_t payload_size, void* user_data);
|
||||
|
||||
typedef struct GfxComputeTypeDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* optional debug label */
|
||||
GfxComputeFn callback; /* required; called from the render worker thread */
|
||||
void* user_data;
|
||||
} GfxComputeTypeDesc;
|
||||
|
||||
#define GFX_COMPUTE_TYPE_DESC_INIT {sizeof(GfxComputeTypeDesc), NULL, NULL, NULL}
|
||||
|
||||
typedef struct GfxService {
|
||||
ServiceHeader header;
|
||||
|
||||
ModResult (*get_device_info)(ModContext* ctx, GfxDeviceInfo* out_info);
|
||||
void* (*get_proc_address)(ModContext* ctx, const char* name);
|
||||
|
||||
ModResult (*register_draw_type)(
|
||||
ModContext* ctx, const GfxDrawTypeDesc* desc, GfxDrawTypeHandle* out_handle);
|
||||
ModResult (*unregister_draw_type)(ModContext* ctx, GfxDrawTypeHandle handle);
|
||||
ModResult (*push_draw)(
|
||||
ModContext* ctx, GfxDrawTypeHandle handle, const void* payload, size_t payload_size);
|
||||
|
||||
ModResult (*register_compute_type)(
|
||||
ModContext* ctx, const GfxComputeTypeDesc* desc, GfxComputeTypeHandle* out_handle);
|
||||
ModResult (*unregister_compute_type)(ModContext* ctx, GfxComputeTypeHandle handle);
|
||||
ModResult (*push_compute)(
|
||||
ModContext* ctx, GfxComputeTypeHandle handle, const void* payload, size_t payload_size);
|
||||
|
||||
ModResult (*push_verts)(
|
||||
ModContext* ctx, const void* data, size_t size, size_t alignment, GfxRange* out_range);
|
||||
ModResult (*push_indices)(
|
||||
ModContext* ctx, const void* data, size_t size, size_t alignment, GfxRange* out_range);
|
||||
ModResult (*push_uniform)(ModContext* ctx, const void* data, size_t size, GfxRange* out_range);
|
||||
ModResult (*push_storage)(ModContext* ctx, const void* data, size_t size, GfxRange* out_range);
|
||||
|
||||
ModResult (*register_stage_hook)(ModContext* ctx, GfxStage stage, const GfxStageHookDesc* desc,
|
||||
GfxStageHookHandle* out_handle);
|
||||
ModResult (*unregister_stage_hook)(ModContext* ctx, GfxStageHookHandle handle);
|
||||
|
||||
ModResult (*resolve_pass)(
|
||||
ModContext* ctx, const GfxResolveDesc* desc, GfxResolvedTargets* out_targets);
|
||||
ModResult (*create_pass)(ModContext* ctx, uint32_t width, uint32_t height);
|
||||
} GfxService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<GfxService> {
|
||||
static constexpr const char* id = GFX_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GFX_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* Intercept game functions by address. Prefer the typed helpers in mods/hook.hpp
|
||||
* (hook_add_pre/hook_add_post/hook_replace over a &Class::method): they generate the
|
||||
* trampoline and hide install/dispatch, which are the low-level primitives those helpers
|
||||
* build. resolve() maps a symbol name to an address for targets you can't name at compile time
|
||||
* (file-local statics included).
|
||||
*
|
||||
* Every call is game-thread-only. Install and removal must run with no hooked function on the
|
||||
* stack; the loader guarantees this by applying mod lifecycle changes between frames, which is
|
||||
* why hooking a function that never returns (the outermost loop) makes a mod un-unloadable.
|
||||
*/
|
||||
|
||||
#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook"
|
||||
#define HOOK_SERVICE_MAJOR 1u
|
||||
#define HOOK_SERVICE_MINOR 0u
|
||||
|
||||
/* Symbol flags reported by resolve() */
|
||||
typedef enum HookSymbolFlags {
|
||||
HOOK_SYMBOL_CODE = 1u << 0u,
|
||||
HOOK_SYMBOL_DATA = 1u << 1u,
|
||||
/* Not exported/dynamically visible: hookable, but never linkable. */
|
||||
HOOK_SYMBOL_LOCAL = 1u << 2u,
|
||||
/* Other names share this address (ICF fold/alias): a hook intercepts them all. */
|
||||
HOOK_SYMBOL_MULTI_NAME = 1u << 3u,
|
||||
/* Resolved through a demangled display-name alias rather than the real symbol. */
|
||||
HOOK_SYMBOL_DISPLAY = 1u << 6u,
|
||||
} HookSymbolFlags;
|
||||
|
||||
/* A pre-hook's return value: whether to run the original function. */
|
||||
typedef enum HookAction {
|
||||
HOOK_CONTINUE = 0, /* run the original (and any lower-priority pre-hooks) */
|
||||
HOOK_SKIP_ORIGINAL = 1, /* cancel the original and remaining pre-hooks; post-hooks still run */
|
||||
} HookAction;
|
||||
|
||||
/* How replace resolves a second replace-hook on a target that already has one. */
|
||||
typedef enum HookReplacePolicy {
|
||||
HOOK_REPLACE_CONFLICT = 0, /* refuse with MOD_CONFLICT (the default) */
|
||||
HOOK_REPLACE_PRIORITY = 1, /* take over only if this options.priority is strictly higher */
|
||||
HOOK_REPLACE_OVERRIDE = 2, /* take over unconditionally */
|
||||
} HookReplacePolicy;
|
||||
|
||||
/*
|
||||
* Hook callbacks. `args` is an array of pointers to the call's arguments (index 0 is `this`
|
||||
* for member functions); `retval` points at the return slot (NULL for void). Read and write
|
||||
* them through dusk::mods::arg<T> / arg_ref<T> from mods/hook.hpp. `userdata` is the pointer
|
||||
* from HookOptions. All run on the game thread, in the hooked call's own stack frame.
|
||||
*/
|
||||
typedef HookAction (*HookPreFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
typedef void (*HookPostFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
typedef void (*HookReplaceFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
|
||||
typedef struct HookOptions {
|
||||
uint32_t struct_size;
|
||||
/* Higher runs first; ties break by registration order. Applies to pre/post ordering and,
|
||||
* with HOOK_REPLACE_PRIORITY, to replace-hook takeover. */
|
||||
int32_t priority;
|
||||
HookReplacePolicy replace_policy;
|
||||
void* userdata; /* passed back to the callback */
|
||||
} HookOptions;
|
||||
|
||||
#define HOOK_OPTIONS_INIT {sizeof(HookOptions), 0, HOOK_REPLACE_CONFLICT, NULL}
|
||||
|
||||
typedef struct HookService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Install a trampoline detour on fn_addr and return the address to call the original through in
|
||||
* *out_original_fn. The typed helpers generate the trampoline and call this; mods normally
|
||||
* don't. The first mod to install a given target owns the live detour; later mods register as
|
||||
* candidates so a hook survives the owner unloading (the detour is handed off and every
|
||||
* original pointer is rewritten). Idempotent per (mod, out slot).
|
||||
*/
|
||||
ModResult (*install)(
|
||||
ModContext* ctx, void* fn_addr, void* trampoline_fn, void** out_original_fn);
|
||||
|
||||
/*
|
||||
* Register a callback on an already-installed target. Pre runs before the original (and can
|
||||
* cancel it), post runs after (even if cancelled). Any number of mods may add pre/post to the
|
||||
* same target; they run in priority then registration order. replace installs a single
|
||||
* substitute for the original, managed by options.replace_policy, MOD_CONFLICT if refused.
|
||||
*/
|
||||
ModResult (*add_pre)(
|
||||
ModContext* ctx, void* fn_addr, HookPreFn callback, const HookOptions* options);
|
||||
ModResult (*add_post)(
|
||||
ModContext* ctx, void* fn_addr, HookPostFn callback, const HookOptions* options);
|
||||
ModResult (*replace)(
|
||||
ModContext* ctx, void* fn_addr, HookReplaceFn callback, const HookOptions* options);
|
||||
|
||||
/*
|
||||
* Run the registered callbacks for a target. The generated trampoline calls these; they
|
||||
* are not a mod-facing entry point. dispatch_pre reports through *out_skip_original
|
||||
* whether the original should be skipped (a pre-hook returned HOOK_SKIP_ORIGINAL, or a
|
||||
* replace-hook ran).
|
||||
*/
|
||||
ModResult (*dispatch_pre)(
|
||||
ModContext* ctx, void* fn_addr, void* args, void* retval, int* out_skip_original);
|
||||
ModResult (*dispatch_post)(ModContext* ctx, void* fn_addr, void* args, void* retval);
|
||||
|
||||
/*
|
||||
* Resolve a game symbol by name from the symbol manifest, including non-exported (static)
|
||||
* functions. Names can be either the platform's mangled name (i.e. the name passed to dlopen;
|
||||
* no Mach-O leading underscore) or the qualified function name without parameters (e.g.
|
||||
* "daAlink_c::execute"). out_flags (optional) receives HookSymbolFlags.
|
||||
*
|
||||
* Results: MOD_OK; MOD_UNSUPPORTED (no manifest for this build, missing or stale);
|
||||
* MOD_UNAVAILABLE (symbol not found); MOD_CONFLICT (name maps to more than one address: C++
|
||||
* overloads or per-TU statics; use the mangled name).
|
||||
*/
|
||||
ModResult (*resolve)(
|
||||
ModContext* ctx, const char* symbol, void** out_addr, HookSymbolFlags* out_flags);
|
||||
} HookService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<HookService> {
|
||||
static constexpr const char* id = HOOK_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* The host service: the calling mod's identity and its runtime interface to the loader.
|
||||
* Always available; every other service can be reached from it.
|
||||
*/
|
||||
|
||||
#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host"
|
||||
#define HOST_SERVICE_MAJOR 2u
|
||||
#define HOST_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* Ignore unknown values: later service minors may add events.
|
||||
*/
|
||||
typedef enum ModLifecycleEvent {
|
||||
/*
|
||||
* The subject mod is gone: its mod_shutdown has run (when it initialized at all) and
|
||||
* every service has already dropped the state it held for it. The subject's library is
|
||||
* still mapped, so pointers into it are valid to compare against, but they must not be
|
||||
* called or dereferenced after the callback returns. Drop everything keyed to the
|
||||
* mod: callbacks it registered, its ModContext*, state indexed by it.
|
||||
*/
|
||||
MOD_LIFECYCLE_DETACHED = 0,
|
||||
} ModLifecycleEvent;
|
||||
|
||||
/*
|
||||
* ctx is the watching mod's own context; subject identifies the mod the event is about.
|
||||
* subject_id is valid only for the duration of the call.
|
||||
*/
|
||||
typedef void (*ModLifecycleFn)(ModContext* ctx, ModContext* subject, const char* subject_id,
|
||||
ModLifecycleEvent event, void* user_data);
|
||||
|
||||
typedef struct HostService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Version string of the current Dusklight build. (e.g. "1.4.2") */
|
||||
const char* version;
|
||||
|
||||
/* Build id of the running game binary: PDB GUID+age on Windows, LC_UUID on macOS, GNU build-id
|
||||
* on Linux. May be empty (len 0) if the identity could not be determined. */
|
||||
const uint8_t* build_id;
|
||||
uint32_t build_id_len;
|
||||
|
||||
/*
|
||||
* Look up a service by id at call time. Unlike a manifest import, this sees whatever is
|
||||
* currently published and carries no initialization-order guarantee (see mods/api.h).
|
||||
* MOD_UNAVAILABLE if no matching service is published; *out_service is null on failure.
|
||||
*/
|
||||
ModResult (*get_service)(ModContext* ctx, const char* service_id, uint16_t major_version,
|
||||
uint16_t min_minor_version, const void** out_service);
|
||||
|
||||
/*
|
||||
* Publish a service the calling mod declared as a deferred export in its manifest.
|
||||
* Must happen during mod_initialize so importers can resolve it; `service` must stay
|
||||
* valid until the mod shuts down.
|
||||
*/
|
||||
ModResult (*publish_service)(
|
||||
ModContext* ctx, const char* service_id, uint16_t major_version, const void* service);
|
||||
|
||||
/*
|
||||
* Report an unrecoverable failure. The calling mod's services stop resolving immediately
|
||||
* and the loader fully disables it at the next safe point; `message` is shown to the user.
|
||||
* Safe to call from any mod callback.
|
||||
*/
|
||||
void (*fail)(ModContext* ctx, ModResult code, const char* message);
|
||||
|
||||
/*
|
||||
* The calling mod's manifest metadata. Returned strings remain valid while the mod is
|
||||
* loaded.
|
||||
*/
|
||||
const char* (*mod_id)(ModContext* ctx);
|
||||
const char* (*mod_name)(ModContext* ctx);
|
||||
const char* (*mod_version)(ModContext* ctx);
|
||||
|
||||
/*
|
||||
* A writable scratch directory reserved for the calling mod. Contents survive disable
|
||||
* and reload within a session, but the directory is wiped at game startup.
|
||||
*/
|
||||
const char* (*mod_dir)(ModContext* ctx);
|
||||
|
||||
/*
|
||||
* Observe other mods' lifecycle events. Any mod whose service hands out per-caller state
|
||||
* (registrations, callbacks, handles) should watch for MOD_LIFECYCLE_DETACHED and drop what it
|
||||
* holds for the subject.
|
||||
*
|
||||
* Callbacks fire on the game thread at a lifecycle safe point (never mid-frame), for
|
||||
* every mod but the watcher itself (use mod_shutdown for self-cleanup).
|
||||
*/
|
||||
ModResult (*watch_mod_lifecycle)(
|
||||
ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle);
|
||||
ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle);
|
||||
} HostService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<HostService> {
|
||||
static constexpr const char* id = HOST_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOST_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* Logging into the game's console and log files. Messages are attributed to the calling mod
|
||||
* (prefixed with its ID).
|
||||
*/
|
||||
|
||||
#define LOG_SERVICE_ID "dev.twilitrealm.dusklight.log"
|
||||
#define LOG_SERVICE_MAJOR 1u
|
||||
#define LOG_SERVICE_MINOR 0u
|
||||
|
||||
typedef enum LogLevel {
|
||||
LOG_LEVEL_TRACE = 0,
|
||||
LOG_LEVEL_DEBUG = 1,
|
||||
LOG_LEVEL_INFO = 2,
|
||||
LOG_LEVEL_WARN = 3,
|
||||
LOG_LEVEL_ERROR = 4,
|
||||
} LogLevel;
|
||||
|
||||
typedef struct LogService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Write a log message at the given level.
|
||||
* `message` is a plain UTF-8 string and is copied before returning.
|
||||
*/
|
||||
void (*write)(ModContext* ctx, LogLevel level, const char* message);
|
||||
|
||||
/* Per-level shorthands for write. */
|
||||
void (*trace)(ModContext* ctx, const char* message);
|
||||
void (*debug)(ModContext* ctx, const char* message);
|
||||
void (*info)(ModContext* ctx, const char* message);
|
||||
void (*warn)(ModContext* ctx, const char* message);
|
||||
void (*error)(ModContext* ctx, const char* message);
|
||||
} LogService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<LogService> {
|
||||
static constexpr const char* id = LOG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = LOG_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay"
|
||||
#define OVERLAY_SERVICE_MAJOR 1u
|
||||
#define OVERLAY_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for a runtime overlay registration. 0 is never a valid handle. */
|
||||
typedef uint64_t OverlayHandle;
|
||||
|
||||
/*
|
||||
* Runtime DVD file overlays.
|
||||
*
|
||||
* Registrations are owned by the calling mod and removed automatically when it is disabled,
|
||||
* reloaded, or fails. Changes are applied at the next frame boundary; data the game has already
|
||||
* read stays in memory until it re-reads the file (sometimes on scene reload, sometimes on
|
||||
* restart).
|
||||
*
|
||||
* disc_path names the file to overlay: absolute with a leading '/', matched against the disc
|
||||
* case-insensitively (e.g. "/res/Stage/R04_00.arc"). Paths that do not exist on the disc are added
|
||||
* as new files.
|
||||
*
|
||||
* If multiple sources overlay the same path, the last one wins: a mod's runtime registrations beat
|
||||
* its static overlay/ files, and later-loaded mods beat earlier ones.
|
||||
*/
|
||||
typedef struct OverlayService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Overlay disc_path with a file from the calling mod's bundle (bundle-relative path, e.g.
|
||||
* "res/replacement.arc"). The file's contents are read lazily on each open, so the bundle
|
||||
* file must not change size while registered.
|
||||
*/
|
||||
ModResult (*add_file)(
|
||||
ModContext* ctx, const char* disc_path, const char* bundle_path, OverlayHandle* out_handle);
|
||||
|
||||
/*
|
||||
* Overlay disc_path with a caller-owned buffer. The data is copied; the caller may free it
|
||||
* as soon as this returns.
|
||||
*/
|
||||
ModResult (*add_buffer)(ModContext* ctx, const char* disc_path, const void* data, size_t size,
|
||||
OverlayHandle* out_handle);
|
||||
|
||||
/* Remove a runtime overlay previously added by the calling mod. */
|
||||
ModResult (*remove)(ModContext* ctx, OverlayHandle handle);
|
||||
} OverlayService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<OverlayService> {
|
||||
static constexpr const char* id = OVERLAY_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = OVERLAY_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* Read-only access to the res/ tree of the calling mod's own bundle. Reload serves the new
|
||||
* bundle's contents. For writable storage, use HostService::mod_dir.
|
||||
*/
|
||||
|
||||
#define RESOURCE_SERVICE_ID "dev.twilitrealm.dusklight.resource"
|
||||
#define RESOURCE_SERVICE_MAJOR 1u
|
||||
#define RESOURCE_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* A loaded resource, allocated by the service. Return every successful load with free;
|
||||
* buffers still live when the mod is disabled or reloaded are reclaimed with a warning.
|
||||
*/
|
||||
typedef struct ResourceBuffer {
|
||||
uint32_t struct_size;
|
||||
void* data;
|
||||
size_t size;
|
||||
} ResourceBuffer;
|
||||
|
||||
#define RESOURCE_BUFFER_INIT {sizeof(ResourceBuffer), NULL, 0u}
|
||||
|
||||
typedef struct ResourceService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Load a file into a fresh allocation. `relative_path` is resolved against the bundle's
|
||||
* res/ directory. Absolute paths and ".." are rejected. MOD_UNAVAILABLE if the file does not
|
||||
* exist. An empty file loads as data == NULL with size 0. Previous contents of `out_buffer` are
|
||||
* overwritten, not freed.
|
||||
*/
|
||||
ModResult (*load)(ModContext* ctx, const char* relative_path, ResourceBuffer* out_buffer);
|
||||
|
||||
/*
|
||||
* Release a loaded buffer and reset it to the empty state. Safe to call on an empty or
|
||||
* already-freed buffer.
|
||||
*/
|
||||
void (*free)(ModContext* ctx, ResourceBuffer* buffer);
|
||||
} ResourceService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<ResourceService> {
|
||||
static constexpr const char* id = RESOURCE_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = RESOURCE_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture"
|
||||
#define TEXTURE_SERVICE_MAJOR 1u
|
||||
#define TEXTURE_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for a runtime texture replacement registration. 0 is never a valid handle. */
|
||||
typedef uint64_t TextureReplacementHandle;
|
||||
|
||||
typedef enum TextureKeyKind {
|
||||
/* Match a texture by the address of its in-memory GX texel data. */
|
||||
TEXTURE_KEY_POINTER = 0,
|
||||
/* Match by content: XXH64 of the base mip level (and of the referenced TLUT range for
|
||||
* palette formats), as encoded in replacement filenames / texture dumps. */
|
||||
TEXTURE_KEY_SOURCE = 1,
|
||||
} TextureKeyKind;
|
||||
|
||||
/* Wildcard values for TEXTURE_KEY_SOURCE hashes ("$" in the filename convention). */
|
||||
#define TEXTURE_HASH_WILDCARD UINT64_C(0xFFFFFFFFFFFFFFFF)
|
||||
#define TEXTURE_TLUT_WILDCARD UINT64_C(0xFFFFFFFFFFFFFFFE)
|
||||
|
||||
typedef struct TextureKey {
|
||||
uint32_t struct_size;
|
||||
TextureKeyKind kind;
|
||||
const void* pointer; /* TEXTURE_KEY_POINTER only */
|
||||
uint64_t texture_hash; /* TEXTURE_KEY_SOURCE */
|
||||
uint64_t tlut_hash; /* TEXTURE_KEY_SOURCE, palette formats only */
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t gx_format;
|
||||
bool has_tlut;
|
||||
} TextureKey;
|
||||
|
||||
#define TEXTURE_KEY_INIT {sizeof(TextureKey), TEXTURE_KEY_POINTER, NULL, 0u, 0u, 0u, 0u, 0u, false}
|
||||
|
||||
typedef struct TextureData {
|
||||
uint32_t struct_size;
|
||||
const void* data; /* texel data laid out in gx_format; copied by the service */
|
||||
size_t size;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t mip_count;
|
||||
uint32_t gx_format; /* any GX texture format supported by Aurora's converter */
|
||||
} TextureData;
|
||||
|
||||
#define TEXTURE_DATA_INIT {sizeof(TextureData), NULL, 0u, 0u, 0u, 1u, 0u}
|
||||
|
||||
/*
|
||||
* Runtime texture replacements.
|
||||
*
|
||||
* Registrations are owned by the calling mod and removed automatically when it is disabled,
|
||||
* reloaded, or fails. When multiple sources replace the same texture, the highest priority wins:
|
||||
* later-loaded mods beat earlier ones, and any mod beats the user's texture_replacements config
|
||||
* directory. Files shipped in a mod's textures/ directory register automatically with the same
|
||||
* ownership and priority; this service is for replacements decided at runtime.
|
||||
*/
|
||||
typedef struct TextureService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Register a replacement from raw texel data. The data is copied; the caller may free it as
|
||||
* soon as this returns. */
|
||||
ModResult (*register_data)(ModContext* ctx, const TextureKey* key, const TextureData* data,
|
||||
TextureReplacementHandle* out_handle);
|
||||
|
||||
/*
|
||||
* Register a replacement from an encoded .dds/.png inside the calling mod's bundle. The
|
||||
* filename encodes the key (same convention as the texture_replacements directory, e.g.
|
||||
* "tex1_{w}x{h}_{hash}_{fmt}.dds"); "_mipN" sidecars next to it are picked up automatically.
|
||||
* The file is decoded lazily on first use by the renderer.
|
||||
*/
|
||||
ModResult (*register_file)(ModContext* ctx, const char* bundle_path,
|
||||
TextureReplacementHandle* out_handle);
|
||||
|
||||
/* Remove a replacement previously registered by the calling mod. */
|
||||
ModResult (*unregister)(ModContext* ctx, TextureReplacementHandle handle);
|
||||
} TextureService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<TextureService> {
|
||||
static constexpr const char* id = TEXTURE_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = TEXTURE_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,284 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
#include "mods/svc/config.h"
|
||||
|
||||
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
|
||||
#define UI_SERVICE_MAJOR 1u
|
||||
#define UI_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, scoped
|
||||
* RCSS stylesheets and menu bar tabs.
|
||||
*
|
||||
* All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI
|
||||
* callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with
|
||||
* MOD_INVALID_ARGUMENT. Element handles die with the content that owns them: a panel or tab rebuild
|
||||
* destroys the previous build's elements, so re-acquire handles inside the build callback rather
|
||||
* than caching them. Strings are UTF-8 and, in both directions, only valid for the duration of the
|
||||
* call.
|
||||
*/
|
||||
|
||||
/* 0 is never a valid handle. */
|
||||
typedef uint64_t UiWindowHandle;
|
||||
typedef uint64_t UiDialogHandle;
|
||||
typedef uint64_t UiElementHandle;
|
||||
typedef uint64_t UiStyleHandle;
|
||||
typedef uint64_t UiMenuTabHandle;
|
||||
|
||||
typedef enum UiStyleScope {
|
||||
UI_SCOPE_PRELAUNCH = 0, /* the pre-launch menu */
|
||||
UI_SCOPE_WINDOW = 1, /* every tabbed/small window, host and mod alike */
|
||||
UI_SCOPE_MENU_BAR = 2, /* the in-game menu bar */
|
||||
UI_SCOPE_OVERLAY = 3, /* the passive overlay (toasts, FPS counter, timers) */
|
||||
UI_SCOPE_TOUCH_CONTROLS = 4, /* touch controls and their editor */
|
||||
UI_SCOPE_GRAPHICS_TUNER = 5, /* the graphics tuner overlay window */
|
||||
} UiStyleScope;
|
||||
|
||||
typedef enum UiDialogVariant {
|
||||
UI_DIALOG_NORMAL = 0,
|
||||
UI_DIALOG_WARNING = 1, /* warning icon by default */
|
||||
UI_DIALOG_DANGER = 2, /* red styling, error icon by default */
|
||||
} UiDialogVariant;
|
||||
|
||||
typedef enum UiControlKind {
|
||||
UI_CONTROL_BUTTON = 0, /* action button (on_pressed) */
|
||||
UI_CONTROL_TOGGLE = 1, /* boolean on/off */
|
||||
UI_CONTROL_NUMBER = 2, /* integer stepper with min/max/step */
|
||||
UI_CONTROL_STRING = 3, /* text input */
|
||||
UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */
|
||||
} UiControlKind;
|
||||
|
||||
typedef enum UiControlBinding {
|
||||
/* Values flow through the `get`/`set` callbacks. Getters are polled every frame while the
|
||||
control is visible and must be cheap. */
|
||||
UI_BINDING_CALLBACKS = 0,
|
||||
/* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod)
|
||||
* directly: persistence, change notifications and the modified indicator (value != default) are
|
||||
* wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and
|
||||
* SELECT = int, STRING = string. Float vars are not bindable; use callbacks. */
|
||||
UI_BINDING_CONFIG_VAR = 1,
|
||||
} UiControlBinding;
|
||||
|
||||
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
|
||||
* reads string_value. string_value passed to a setter is only valid during the call; a getter
|
||||
* should point it at storage owned by the mod (e.g. a static buffer) that stays valid until the
|
||||
* next call into the mod — the host copies it right after the getter returns. */
|
||||
typedef struct UiControlValue {
|
||||
uint32_t struct_size;
|
||||
bool bool_value;
|
||||
int64_t int_value;
|
||||
const char* string_value;
|
||||
} UiControlValue;
|
||||
|
||||
#define UI_CONTROL_VALUE_INIT {sizeof(UiControlValue), false, 0, NULL}
|
||||
|
||||
typedef void (*UiControlGetFn)(ModContext* ctx, void* user_data, UiControlValue* out_value);
|
||||
typedef void (*UiControlSetFn)(ModContext* ctx, void* user_data, const UiControlValue* value);
|
||||
/* Polled every frame while the control is visible. */
|
||||
typedef bool (*UiPredicateFn)(ModContext* ctx, void* user_data);
|
||||
typedef void (*UiPressedFn)(ModContext* ctx, void* user_data);
|
||||
|
||||
typedef struct UiControlDesc {
|
||||
uint32_t struct_size;
|
||||
UiControlKind kind;
|
||||
/* Row label (plain text). Required. */
|
||||
const char* label;
|
||||
/* Optional RML shown as contextual help when the control is focused or hovered. Only rendered
|
||||
* where a help pane exists (mod window tabs). */
|
||||
const char* help_rml;
|
||||
UiControlBinding binding; /* ignored for BUTTON */
|
||||
ConfigVarHandle config_var; /* UI_BINDING_CONFIG_VAR */
|
||||
UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
|
||||
UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
|
||||
UiPressedFn on_pressed; /* BUTTON only. Required for BUTTON. */
|
||||
UiPredicateFn is_disabled; /* optional */
|
||||
/* Optional override for the modified indicator. CONFIG_VAR controls derive it from value !=
|
||||
* default when this is NULL. */
|
||||
UiPredicateFn is_modified;
|
||||
/* Passed to every callback above. */
|
||||
void* user_data;
|
||||
/* NUMBER: inclusive clamp range and step. min == max means the defaults (0 .. INT32_MAX); step
|
||||
* < 1 means 1. */
|
||||
int64_t min;
|
||||
int64_t max;
|
||||
int64_t step;
|
||||
const char* prefix; /* NUMBER: optional text before the value */
|
||||
const char* suffix; /* NUMBER: optional text after the value */
|
||||
/* SELECT: option labels (plain text). Required for SELECT. SELECT controls
|
||||
* present their options in the help pane, so they are only available where
|
||||
* one exists (mod window tabs); MOD_UNSUPPORTED elsewhere. */
|
||||
const char* const* options;
|
||||
size_t option_count;
|
||||
int32_t max_length; /* STRING: maximum input length; < 1 means unlimited */
|
||||
} UiControlDesc;
|
||||
|
||||
#define UI_CONTROL_DESC_INIT \
|
||||
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
|
||||
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0}
|
||||
|
||||
/* Build the panel contents. `panel` accepts the pane_add_* functions; it and
|
||||
* every element created in it are destroyed (handles invalidated) whenever the
|
||||
* panel is rebuilt, e.g. on tab switches. A non-MOD_OK result fails the mod. */
|
||||
typedef ModResult (*UiPanelBuildFn)(
|
||||
ModContext* ctx, UiElementHandle panel, void* user_data, ModError* out_error);
|
||||
/* Called every frame while the panel is the visible tab. */
|
||||
typedef ModResult (*UiPanelUpdateFn)(ModContext* ctx, void* user_data, ModError* out_error);
|
||||
|
||||
/* A panel rendered inside the calling mod's tab of the host Mods window. */
|
||||
typedef struct UiModsPanelDesc {
|
||||
uint32_t struct_size;
|
||||
UiPanelBuildFn build; /* required */
|
||||
UiPanelUpdateFn update;
|
||||
void* user_data;
|
||||
} UiModsPanelDesc;
|
||||
|
||||
#define UI_MODS_PANEL_DESC_INIT {sizeof(UiModsPanelDesc), NULL, NULL, NULL}
|
||||
|
||||
/* Build one tab of a mod window. `left_pane` is the interactive column,
|
||||
* `right_pane` shows contextual help (controls' help_rml and SELECT options
|
||||
* render there). Rebuilt on every tab activation. */
|
||||
typedef ModResult (*UiTabBuildFn)(ModContext* ctx, UiWindowHandle window, UiElementHandle left_pane,
|
||||
UiElementHandle right_pane, void* user_data, ModError* out_error);
|
||||
|
||||
typedef struct UiTabDesc {
|
||||
uint32_t struct_size;
|
||||
const char* title; /* required */
|
||||
UiTabBuildFn build; /* required */
|
||||
UiPanelUpdateFn update;
|
||||
void* user_data;
|
||||
} UiTabDesc;
|
||||
|
||||
#define UI_TAB_DESC_INIT {sizeof(UiTabDesc), NULL, NULL, NULL, NULL}
|
||||
|
||||
typedef void (*UiWindowClosedFn)(ModContext* ctx, UiWindowHandle window, void* user_data);
|
||||
|
||||
typedef struct UiWindowDesc {
|
||||
uint32_t struct_size;
|
||||
const UiTabDesc* tabs; /* at least one */
|
||||
size_t tab_count;
|
||||
/* Optional RCSS applied to this window's document only (on top of any
|
||||
* UI_SCOPE_WINDOW sheets, which apply automatically). */
|
||||
const char* rcss;
|
||||
/* Fired when the window is destroyed by user close or window_close. Not
|
||||
* fired during owning-mod teardown/shutdown. The handle is already invalid. */
|
||||
UiWindowClosedFn on_closed;
|
||||
void* user_data;
|
||||
} UiWindowDesc;
|
||||
|
||||
#define UI_WINDOW_DESC_INIT {sizeof(UiWindowDesc), NULL, 0u, NULL, NULL, NULL}
|
||||
|
||||
typedef void (*UiDialogActionFn)(ModContext* ctx, UiDialogHandle dialog, void* user_data);
|
||||
|
||||
/* Note: array element without struct_size; a future change requires appending
|
||||
* a v2 desc struct rather than growing this one. */
|
||||
typedef struct UiDialogAction {
|
||||
const char* label; /* required */
|
||||
UiDialogActionFn on_pressed;
|
||||
void* user_data;
|
||||
bool keep_open; /* false = the dialog closes after on_pressed returns */
|
||||
} UiDialogAction;
|
||||
|
||||
typedef struct UiDialogDesc {
|
||||
uint32_t struct_size;
|
||||
const char* title; /* plain text; required */
|
||||
const char* body_rml; /* RML body; required */
|
||||
UiDialogVariant variant;
|
||||
/* Optional icon name overriding the variant default: "warning", "error",
|
||||
* "question-mark", "verifying", "celebration". */
|
||||
const char* icon;
|
||||
const UiDialogAction* actions; /* at least one */
|
||||
size_t action_count;
|
||||
/* Fired on cancel (B/Escape) before the dialog closes; the dialog always
|
||||
* closes on dismiss. */
|
||||
UiDialogActionFn on_dismiss;
|
||||
void* user_data; /* passed to on_dismiss */
|
||||
} UiDialogDesc;
|
||||
|
||||
#define UI_DIALOG_DESC_INIT \
|
||||
{sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL}
|
||||
|
||||
/* A tab added to the in-game menu bar. */
|
||||
typedef struct UiMenuTabDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* plain text; required */
|
||||
UiPressedFn on_selected; /* fired when the user activates the tab; required */
|
||||
void* user_data;
|
||||
} UiMenuTabDesc;
|
||||
|
||||
#define UI_MENU_TAB_DESC_INIT {sizeof(UiMenuTabDesc), NULL, NULL, NULL}
|
||||
|
||||
typedef struct UiService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Register or replace the panel shown in the calling mod's Mods-window tab. */
|
||||
ModResult (*register_mods_panel)(ModContext* ctx, const UiModsPanelDesc* desc);
|
||||
|
||||
/* Content builders. `pane` is a panel or tab pane handle; out_elem (where
|
||||
* present, optional) receives a handle for later elem_set_* updates. */
|
||||
ModResult (*pane_add_section)(ModContext* ctx, UiElementHandle pane, const char* title);
|
||||
ModResult (*pane_add_text)(
|
||||
ModContext* ctx, UiElementHandle pane, const char* text, UiElementHandle* out_elem);
|
||||
ModResult (*pane_add_rml)(
|
||||
ModContext* ctx, UiElementHandle pane, const char* rml, UiElementHandle* out_elem);
|
||||
ModResult (*pane_add_progress)(
|
||||
ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem);
|
||||
ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc,
|
||||
UiElementHandle* out_elem);
|
||||
|
||||
/* Element updates. The handle kind must match the setter (text/rml on text
|
||||
* rows, progress on progress bars). */
|
||||
ModResult (*elem_set_text)(ModContext* ctx, UiElementHandle elem, const char* text);
|
||||
ModResult (*elem_set_rml)(ModContext* ctx, UiElementHandle elem, const char* rml);
|
||||
ModResult (*elem_set_progress)(ModContext* ctx, UiElementHandle elem, float value);
|
||||
/* Set or clear an RCSS class on any element handle (rows, progress bars,
|
||||
* controls, panes), for styling via scoped or window RCSS. */
|
||||
ModResult (*elem_set_class)(
|
||||
ModContext* ctx, UiElementHandle elem, const char* name, bool active);
|
||||
|
||||
/* Push a tabbed two-pane window onto the document stack and show it. */
|
||||
ModResult (*window_push)(ModContext* ctx, const UiWindowDesc* desc, UiWindowHandle* out_window);
|
||||
ModResult (*window_close)(ModContext* ctx, UiWindowHandle window);
|
||||
|
||||
/* Push a modal dialog onto the document stack and show it. */
|
||||
ModResult (*dialog_push)(ModContext* ctx, const UiDialogDesc* desc, UiDialogHandle* out_dialog);
|
||||
ModResult (*dialog_close)(ModContext* ctx, UiDialogHandle dialog);
|
||||
ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml);
|
||||
/* Replace the dialog icon ("" removes it; names as in UiDialogDesc.icon). */
|
||||
ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon);
|
||||
/* Append one action button (same callback rules as at push). */
|
||||
ModResult (*dialog_add_action)(
|
||||
ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action);
|
||||
|
||||
/* Whether any focus-stack document is currently visible (visible documents block gamepad
|
||||
* input). */
|
||||
ModResult (*is_any_document_visible)(ModContext* ctx, bool* out_visible);
|
||||
|
||||
/* Register an RCSS sheet applied to every document of `scope`, now and in the future, until
|
||||
* unregistered or the calling mod is torn down. Sheets apply in registration order after the
|
||||
* host styles. Fails with MOD_INVALID_ARGUMENT if the RCSS does not parse. */
|
||||
ModResult (*register_styles)(
|
||||
ModContext* ctx, UiStyleScope scope, const char* rcss, UiStyleHandle* out_style);
|
||||
/* Like register_styles, but reads the RCSS from the mod bundle's res/ directory (same path
|
||||
* rules as ResourceService::load). MOD_UNAVAILABLE if the file does not exist. */
|
||||
ModResult (*register_styles_file)(
|
||||
ModContext* ctx, UiStyleScope scope, const char* path, UiStyleHandle* out_style);
|
||||
ModResult (*unregister_styles)(ModContext* ctx, UiStyleHandle style);
|
||||
|
||||
/* Add a tab to the in-game menu bar, on_selected runs with the menu open; pushing a window from
|
||||
* it stacks the window over the menu like the host tabs do. The tab is removed when
|
||||
* unregistered or the mod is torn down. */
|
||||
ModResult (*register_menu_tab)(
|
||||
ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab);
|
||||
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
|
||||
} UiService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<UiService> {
|
||||
static constexpr const char* id = UI_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = UI_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "JSystem/J3DGraphBase/J3DShapeDraw.h"
|
||||
#include "JSystem/J3DAssert.h"
|
||||
#include "JSystem/J3DGraphBase/J3DFifo.h"
|
||||
#include "JSystem/JMath/JMath.h"
|
||||
#include "global.h"
|
||||
#include <mtx.h>
|
||||
#include "dusk/endian_gx.hpp"
|
||||
|
||||
@@ -202,7 +204,7 @@ public:
|
||||
|
||||
static void resetVcdVatCache() { sOldVcdVatCmd = NULL; }
|
||||
|
||||
static void* sOldVcdVatCmd;
|
||||
static DUSK_GAME_DATA void* sOldVcdVatCmd;
|
||||
static bool sEnvelopeFlag;
|
||||
|
||||
private:
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "JSystem/JMath/JMath.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/endian.h"
|
||||
#include "global.h"
|
||||
|
||||
enum J3DSysDrawBuf {
|
||||
/* 0x0 */ J3DSysDrawBuf_Opa,
|
||||
@@ -208,6 +209,6 @@ struct J3DSys {
|
||||
};
|
||||
|
||||
extern u32 j3dDefaultViewNo;
|
||||
extern J3DSys j3dSys;
|
||||
DUSK_GAME_EXTERN J3DSys j3dSys;
|
||||
|
||||
#endif /* J3DSYS_H */
|
||||
|
||||
@@ -12,12 +12,23 @@ struct JORNodeEvent;
|
||||
class JORMContext;
|
||||
class JORServer;
|
||||
|
||||
// NOTE (stable game ABI): these classes stay non-polymorphic outside DEBUG
|
||||
// on purpose. Making them polymorphic under PARTIAL_DEBUG would give every one of the ~250
|
||||
// derived HIO classes a vptr and turn their plain `void genMessage(JORMContext*);`
|
||||
// declarations into implicit virtual overrides whose definitions are #if DEBUG-gated; every
|
||||
// instantiated one then fails to link (missing vtable). Closure types shared with DEBUG TUs
|
||||
// either declare their own unconditional virtuals (vptr in all TUs anyway) or add a
|
||||
// PARTIAL_DEBUG-only virtual dtor for vptr parity (see dAttParam_c).
|
||||
class JOREventListener {
|
||||
public:
|
||||
#if DEBUG
|
||||
JOREventListener() {}
|
||||
#if TARGET_PC
|
||||
virtual void listenPropertyEvent(const JORPropertyEvent*) {}
|
||||
#else
|
||||
virtual void listenPropertyEvent(const JORPropertyEvent*) = 0;
|
||||
#endif
|
||||
#endif
|
||||
};
|
||||
|
||||
class JORReflexible : public JOREventListener {
|
||||
@@ -30,7 +41,11 @@ public:
|
||||
virtual void listenPropertyEvent(const JORPropertyEvent*);
|
||||
virtual void listen(u32, const JOREvent*);
|
||||
virtual void genObjectInfo(const JORGenEvent*);
|
||||
#if TARGET_PC
|
||||
virtual void genMessage(JORMContext*) {}
|
||||
#else
|
||||
virtual void genMessage(JORMContext*) = 0;
|
||||
#endif
|
||||
virtual void listenNodeEvent(const JORNodeEvent*);
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -314,7 +314,7 @@ public:
|
||||
>
|
||||
{
|
||||
TIterator_data_(const TFunctionValue_list_parameter& rParent, const f32* value) {
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
pOwn_ = &rParent;
|
||||
#endif
|
||||
pf_ = value;
|
||||
@@ -372,7 +372,7 @@ public:
|
||||
return (r1.pf_ - r2.pf_) / suData_size;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x00 */ const TFunctionValue_list_parameter* pOwn_;
|
||||
#endif
|
||||
/* 0x00 */ const f32* pf_;
|
||||
@@ -425,7 +425,7 @@ public:
|
||||
>
|
||||
{
|
||||
TIterator_data_(const TFunctionValue_hermite& rParent, const f32* value) {
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
pOwn_ = &rParent;
|
||||
#endif
|
||||
pf_ = value;
|
||||
@@ -491,7 +491,7 @@ public:
|
||||
return (r1.pf_ - r2.pf_) / r1.uSize_;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x00 */ const TFunctionValue_hermite* pOwn_;
|
||||
/* 0x04 */ const f32* pf_;
|
||||
/* 0x08 */ u32 uSize_;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(ao_mod CXX)
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
|
||||
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if (DUSK_MOD_USE_FULL_TREE)
|
||||
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
|
||||
else ()
|
||||
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_mod(ao_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
BUNDLE
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.ao_mod",
|
||||
"name": "[Demo] Ambient Occlusion",
|
||||
"version": "1.0.0",
|
||||
"author": "Twilit Realm",
|
||||
"description": "Ground-truth ambient occlusion (GTAO) computed from the scene depth buffer and composited over the game. Ported from Bevy Engine's SSAO and Intel XeGTAO."
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Fullscreen composite: multiplies the denoised ambient-occlusion visibility over the scene.
|
||||
//
|
||||
// Debug views:
|
||||
// 1 = raw AO visibility as grayscale
|
||||
// 2 = view-space normals reconstructed from depth (keep in sync with gtao.wgsl)
|
||||
// 3 = the preprocessed depth input
|
||||
// 4 = depth staircase detector
|
||||
|
||||
struct Uniforms {
|
||||
projection: mat4x4f,
|
||||
inverse_projection: mat4x4f,
|
||||
size: vec2f, // AO texture size in pixels (may be half the render size)
|
||||
inv_size: vec2f,
|
||||
depth_scale: vec2f,
|
||||
effect_radius: f32,
|
||||
intensity: f32,
|
||||
slice_count: f32,
|
||||
samples_per_slice_side: f32,
|
||||
debug_view: u32,
|
||||
_pad: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var ambient_occlusion: texture_2d<f32>;
|
||||
@group(0) @binding(1) var preprocessed_depth: texture_2d<f32>;
|
||||
@group(0) @binding(2) var scene_depth_raw: texture_2d<f32>;
|
||||
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4f,
|
||||
@location(0) uv: vec2f,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput {
|
||||
// Fullscreen triangle
|
||||
var out: VertexOutput;
|
||||
let uv = vec2f(f32((index << 1u) & 2u), f32(index & 2u));
|
||||
out.position = vec4f(uv * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), 0.0, 1.0);
|
||||
out.uv = uv;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Manual bilinear sample (r32float is unfilterable without optional device features)
|
||||
fn sample_visibility(uv: vec2f) -> f32 {
|
||||
let coordinates = uv * uniforms.size - 0.5;
|
||||
let base = floor(coordinates);
|
||||
let fraction = coordinates - base;
|
||||
let max_coordinates = vec2i(uniforms.size) - 1i;
|
||||
let p00 = clamp(vec2i(base), vec2i(0i), max_coordinates);
|
||||
let p11 = clamp(vec2i(base) + 1i, vec2i(0i), max_coordinates);
|
||||
let v00 = textureLoad(ambient_occlusion, vec2i(p00.x, p00.y), 0i).r;
|
||||
let v10 = textureLoad(ambient_occlusion, vec2i(p11.x, p00.y), 0i).r;
|
||||
let v01 = textureLoad(ambient_occlusion, vec2i(p00.x, p11.y), 0i).r;
|
||||
let v11 = textureLoad(ambient_occlusion, vec2i(p11.x, p11.y), 0i).r;
|
||||
let top = mix(v00, v10, fraction.x);
|
||||
let bottom = mix(v01, v11, fraction.x);
|
||||
return mix(top, bottom, fraction.y);
|
||||
}
|
||||
|
||||
fn load_depth(pixel_coordinates: vec2<i32>) -> f32 {
|
||||
let coordinates = clamp(pixel_coordinates, vec2<i32>(0i), vec2<i32>(uniforms.size) - 1i);
|
||||
return textureLoad(preprocessed_depth, coordinates, 0i).r;
|
||||
}
|
||||
|
||||
fn reconstruct_view_space_position(depth: f32, uv: vec2f) -> vec3f {
|
||||
let clip_xy = vec2f(uv.x * 2.0 - 1.0, 1.0 - 2.0 * uv.y);
|
||||
let t = uniforms.inverse_projection * vec4f(clip_xy, depth, 1.0);
|
||||
return t.xyz / t.w;
|
||||
}
|
||||
|
||||
fn view_position_at(pixel_coordinates: vec2<i32>) -> vec3f {
|
||||
let depth = load_depth(pixel_coordinates);
|
||||
let uv = (vec2f(pixel_coordinates) + 0.5) * uniforms.inv_size;
|
||||
return reconstruct_view_space_position(depth, uv);
|
||||
}
|
||||
|
||||
fn reconstruct_normal(pixel_coordinates: vec2<i32>, pixel_position: vec3f, depth_center: f32) -> vec3f {
|
||||
let depth_left1 = load_depth(pixel_coordinates + vec2<i32>(-1i, 0i));
|
||||
let depth_left2 = load_depth(pixel_coordinates + vec2<i32>(-2i, 0i));
|
||||
let depth_right1 = load_depth(pixel_coordinates + vec2<i32>(1i, 0i));
|
||||
let depth_right2 = load_depth(pixel_coordinates + vec2<i32>(2i, 0i));
|
||||
let depth_top1 = load_depth(pixel_coordinates + vec2<i32>(0i, -1i));
|
||||
let depth_top2 = load_depth(pixel_coordinates + vec2<i32>(0i, -2i));
|
||||
let depth_bottom1 = load_depth(pixel_coordinates + vec2<i32>(0i, 1i));
|
||||
let depth_bottom2 = load_depth(pixel_coordinates + vec2<i32>(0i, 2i));
|
||||
|
||||
let use_left = abs(2.0 * depth_left1 - depth_left2 - depth_center) <
|
||||
abs(2.0 * depth_right1 - depth_right2 - depth_center);
|
||||
let use_top = abs(2.0 * depth_top1 - depth_top2 - depth_center) <
|
||||
abs(2.0 * depth_bottom1 - depth_bottom2 - depth_center);
|
||||
|
||||
var ddx: vec3f;
|
||||
if use_left {
|
||||
ddx = pixel_position - view_position_at(pixel_coordinates + vec2<i32>(-1i, 0i));
|
||||
} else {
|
||||
ddx = view_position_at(pixel_coordinates + vec2<i32>(1i, 0i)) - pixel_position;
|
||||
}
|
||||
var ddy: vec3f;
|
||||
if use_top {
|
||||
ddy = pixel_position - view_position_at(pixel_coordinates + vec2<i32>(0i, -1i));
|
||||
} else {
|
||||
ddy = view_position_at(pixel_coordinates + vec2<i32>(0i, 1i)) - pixel_position;
|
||||
}
|
||||
|
||||
var normal = normalize(cross(ddy, ddx));
|
||||
if dot(normal, pixel_position) > 0.0 {
|
||||
normal = -normal;
|
||||
}
|
||||
return normal;
|
||||
}
|
||||
|
||||
// Raw-snapshot variant of load_depth for the staircase view
|
||||
fn load_raw_depth(pixel_coordinates: vec2<i32>) -> f32 {
|
||||
let size = vec2<i32>(textureDimensions(scene_depth_raw));
|
||||
let coordinates = clamp(pixel_coordinates, vec2<i32>(0i), size - 1i);
|
||||
return textureLoad(scene_depth_raw, coordinates, 0i).r;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
||||
if uniforms.debug_view == 2u {
|
||||
// Reconstructed view-space normals, [-1,1] -> RGB
|
||||
let pixel = vec2<i32>(in.uv * uniforms.size);
|
||||
let depth = load_depth(pixel);
|
||||
let uv = (vec2f(pixel) + 0.5) * uniforms.inv_size;
|
||||
let position = reconstruct_view_space_position(depth, uv);
|
||||
let normal = reconstruct_normal(pixel, position, depth);
|
||||
return vec4f(normal * 0.5 + 0.5, 1.0);
|
||||
}
|
||||
if uniforms.debug_view == 3u {
|
||||
// Preprocessed depth as an exponential distance gradient (white = near, black = far)
|
||||
let pixel = vec2<i32>(in.uv * uniforms.size);
|
||||
let position = view_position_at(pixel);
|
||||
let value = exp(-max(-position.z, 0.0) * 0.0003);
|
||||
return vec4f(value, value, value, 1.0);
|
||||
}
|
||||
if uniforms.debug_view == 4u {
|
||||
// Staircase detector on the raw snapshot depth
|
||||
let size = vec2f(textureDimensions(scene_depth_raw));
|
||||
let pixel = vec2<i32>(in.uv * size);
|
||||
let d_center = load_raw_depth(pixel);
|
||||
let d_left = load_raw_depth(pixel + vec2<i32>(-1i, 0i));
|
||||
let d_right = load_raw_depth(pixel + vec2<i32>(1i, 0i));
|
||||
let d_top = load_raw_depth(pixel + vec2<i32>(0i, -1i));
|
||||
let d_bottom = load_raw_depth(pixel + vec2<i32>(0i, 1i));
|
||||
let gradient_x = abs(d_right - d_left) * 0.5;
|
||||
let curvature_x = abs(d_right - 2.0 * d_center + d_left);
|
||||
let gradient_y = abs(d_bottom - d_top) * 0.5;
|
||||
let curvature_y = abs(d_bottom - 2.0 * d_center + d_top);
|
||||
let ratio_x = curvature_x / max(gradient_x, 1e-12);
|
||||
let ratio_y = curvature_y / max(gradient_y, 1e-12);
|
||||
return vec4f(saturate(ratio_x), saturate(ratio_y), 0.0, 1.0);
|
||||
}
|
||||
|
||||
let visibility = sample_visibility(in.uv);
|
||||
if uniforms.debug_view == 1u {
|
||||
return vec4f(visibility, visibility, visibility, 1.0);
|
||||
}
|
||||
let value = mix(1.0, visibility, uniforms.intensity);
|
||||
return vec4f(value, value, value, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// 3x3 bilaterial filter (edge-preserving blur)
|
||||
// https://people.csail.mit.edu/sparis/bf_course/course_notes.pdf
|
||||
//
|
||||
// Note: Does not use the Gaussian kernel part of a typical bilateral blur
|
||||
// From the paper: "use the information gathered on a neighborhood of 4 x 4 using a bilateral filter for
|
||||
// reconstruction, using _uniform_ convolution weights"
|
||||
//
|
||||
// Note: The paper does a 4x4 (not quite centered) filter, offset by +/- 1 pixel every other frame
|
||||
// XeGTAO does a 3x3 filter, on two pixels at a time per compute thread, applied twice
|
||||
// We do a 3x3 filter, on 1 pixel per compute thread, applied once
|
||||
//
|
||||
// Ported from Bevy Engine, crates/bevy_pbr/src/ssao/spatial_denoise.wgsl (v0.13.2), licensed
|
||||
// MIT OR Apache-2.0 (see res/licenses/), itself derived from Intel XeGTAO (MIT).
|
||||
//
|
||||
// PORT: the textureGather calls are rewritten as explicit per-neighbor textureLoads (r32float
|
||||
// and r32uint are unfilterable); Bevy view uniforms -> the mod's uniform block; r16float -> r32float.
|
||||
|
||||
struct Uniforms {
|
||||
projection: mat4x4f,
|
||||
inverse_projection: mat4x4f,
|
||||
size: vec2f,
|
||||
inv_size: vec2f,
|
||||
depth_scale: vec2f,
|
||||
effect_radius: f32,
|
||||
intensity: f32,
|
||||
slice_count: f32,
|
||||
samples_per_slice_side: f32,
|
||||
debug_view: u32,
|
||||
_pad: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var ambient_occlusion_noisy: texture_2d<f32>;
|
||||
@group(0) @binding(1) var depth_differences: texture_2d<u32>;
|
||||
@group(0) @binding(2) var ambient_occlusion: texture_storage_2d<r32float, write>;
|
||||
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
|
||||
|
||||
fn clamp_coordinates(pixel_coordinates: vec2<i32>) -> vec2<i32> {
|
||||
return clamp(pixel_coordinates, vec2<i32>(0i), vec2<i32>(uniforms.size) - 1i);
|
||||
}
|
||||
|
||||
// Each pixel's packed edge info is (left, right, top, bottom) weights, packed by the GTAO pass.
|
||||
fn load_edges(pixel_coordinates: vec2<i32>) -> vec4<f32> {
|
||||
return unpack4x8unorm(textureLoad(depth_differences, clamp_coordinates(pixel_coordinates), 0i).r);
|
||||
}
|
||||
|
||||
fn load_visibility(pixel_coordinates: vec2<i32>) -> f32 {
|
||||
return textureLoad(ambient_occlusion_noisy, clamp_coordinates(pixel_coordinates), 0i).r;
|
||||
}
|
||||
|
||||
@compute
|
||||
@workgroup_size(8, 8, 1)
|
||||
fn spatial_denoise(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
let pixel_coordinates = vec2<i32>(global_id.xy);
|
||||
|
||||
let left_edges = load_edges(pixel_coordinates + vec2<i32>(-1i, 0i));
|
||||
let right_edges = load_edges(pixel_coordinates + vec2<i32>(1i, 0i));
|
||||
let top_edges = load_edges(pixel_coordinates + vec2<i32>(0i, -1i));
|
||||
let bottom_edges = load_edges(pixel_coordinates + vec2<i32>(0i, 1i));
|
||||
var center_edges = load_edges(pixel_coordinates);
|
||||
// Cross-check each edge against the neighbor's opposing edge weight.
|
||||
center_edges *= vec4<f32>(left_edges.y, right_edges.x, top_edges.w, bottom_edges.z);
|
||||
|
||||
let center_weight = 1.2;
|
||||
let left_weight = center_edges.x;
|
||||
let right_weight = center_edges.y;
|
||||
let top_weight = center_edges.z;
|
||||
let bottom_weight = center_edges.w;
|
||||
let top_left_weight = 0.425 * (top_weight * top_edges.x + left_weight * left_edges.z);
|
||||
let top_right_weight = 0.425 * (top_weight * top_edges.y + right_weight * right_edges.z);
|
||||
let bottom_left_weight = 0.425 * (bottom_weight * bottom_edges.x + left_weight * left_edges.w);
|
||||
let bottom_right_weight = 0.425 * (bottom_weight * bottom_edges.y + right_weight * right_edges.w);
|
||||
|
||||
let center_visibility = load_visibility(pixel_coordinates);
|
||||
let left_visibility = load_visibility(pixel_coordinates + vec2<i32>(-1i, 0i));
|
||||
let right_visibility = load_visibility(pixel_coordinates + vec2<i32>(1i, 0i));
|
||||
let top_visibility = load_visibility(pixel_coordinates + vec2<i32>(0i, -1i));
|
||||
let bottom_visibility = load_visibility(pixel_coordinates + vec2<i32>(0i, 1i));
|
||||
let top_left_visibility = load_visibility(pixel_coordinates + vec2<i32>(-1i, -1i));
|
||||
let top_right_visibility = load_visibility(pixel_coordinates + vec2<i32>(1i, -1i));
|
||||
let bottom_left_visibility = load_visibility(pixel_coordinates + vec2<i32>(-1i, 1i));
|
||||
let bottom_right_visibility = load_visibility(pixel_coordinates + vec2<i32>(1i, 1i));
|
||||
|
||||
// PORT: Bevy sums the center sample unweighted while still counting center_weight in the
|
||||
// denominator; XeGTAO's original weights the value too, which is what we do here.
|
||||
var sum = center_visibility * center_weight;
|
||||
sum += left_visibility * left_weight;
|
||||
sum += right_visibility * right_weight;
|
||||
sum += top_visibility * top_weight;
|
||||
sum += bottom_visibility * bottom_weight;
|
||||
sum += top_left_visibility * top_left_weight;
|
||||
sum += top_right_visibility * top_right_weight;
|
||||
sum += bottom_left_visibility * bottom_left_weight;
|
||||
sum += bottom_right_visibility * bottom_right_weight;
|
||||
|
||||
var sum_weight = center_weight;
|
||||
sum_weight += left_weight;
|
||||
sum_weight += right_weight;
|
||||
sum_weight += top_weight;
|
||||
sum_weight += bottom_weight;
|
||||
sum_weight += top_left_weight;
|
||||
sum_weight += top_right_weight;
|
||||
sum_weight += bottom_left_weight;
|
||||
sum_weight += bottom_right_weight;
|
||||
|
||||
let denoised_visibility = sum / sum_weight;
|
||||
|
||||
textureStore(ambient_occlusion, pixel_coordinates, vec4<f32>(denoised_visibility, 0.0, 0.0, 0.0));
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
// Ground Truth-based Ambient Occlusion (GTAO)
|
||||
// Paper: https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf
|
||||
// Presentation: https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf
|
||||
//
|
||||
// Ported from Bevy Engine, crates/bevy_pbr/src/ssao/gtao.wgsl (v0.13.2), licensed
|
||||
// MIT OR Apache-2.0 (see res/licenses/), itself heavily based on XeGTAO v1.30 from Intel (MIT):
|
||||
// https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli
|
||||
//
|
||||
// PORT:
|
||||
// - Bevy view/globals bindings -> the mod's own uniform block (matrices from Dusklight's
|
||||
// CameraService, WebGPU clip convention, reversed-Z - the same convention Bevy uses).
|
||||
// - Prepass normals -> normals reconstructed from depth (atyuwen's accurate 5-tap method,
|
||||
// https://atyuwen.github.io/posts/normal-reconstruction/).
|
||||
// - Sampler-based reads -> textureLoad (r32float is unfilterable without optional features);
|
||||
// the mip level for the XeGTAO bandwidth optimization is selected explicitly per load.
|
||||
// - effect_radius and slice/sample counts come from uniforms instead of constants/shader defs
|
||||
// (game world units are ~100x larger than Bevy's meters, and quality is a live setting).
|
||||
// - No TEMPORAL_JITTER: the noise index is pinned (no TAA; the spatial denoiser is the only
|
||||
// filter, a configuration XeGTAO supports).
|
||||
// - Storage format r16float -> r32float (core WebGPU storage format).
|
||||
|
||||
struct Uniforms {
|
||||
projection: mat4x4f,
|
||||
inverse_projection: mat4x4f,
|
||||
size: vec2f,
|
||||
inv_size: vec2f,
|
||||
depth_scale: vec2f,
|
||||
effect_radius: f32,
|
||||
intensity: f32,
|
||||
slice_count: f32,
|
||||
samples_per_slice_side: f32,
|
||||
debug_view: u32,
|
||||
_pad: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var preprocessed_depth: texture_2d<f32>;
|
||||
@group(0) @binding(1) var hilbert_index_lut: texture_2d<u32>;
|
||||
@group(0) @binding(2) var ambient_occlusion: texture_storage_2d<r32float, write>;
|
||||
@group(0) @binding(3) var depth_differences: texture_storage_2d<r32uint, write>;
|
||||
@group(0) @binding(4) var<uniform> uniforms: Uniforms;
|
||||
|
||||
const PI: f32 = 3.141592653589793;
|
||||
const HALF_PI: f32 = 1.5707963267948966;
|
||||
|
||||
fn fast_sqrt(x: f32) -> f32 {
|
||||
return bitcast<f32>(0x1fbd1df5 + (bitcast<i32>(x) >> 1u));
|
||||
}
|
||||
|
||||
fn fast_acos(in_x: f32) -> f32 {
|
||||
let x = abs(in_x);
|
||||
var res = -0.156583 * x + HALF_PI;
|
||||
res *= fast_sqrt(1.0 - x);
|
||||
return select(PI - res, res, in_x >= 0.0);
|
||||
}
|
||||
|
||||
fn load_noise(pixel_coordinates: vec2<i32>) -> vec2<f32> {
|
||||
let index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r;
|
||||
// R2 sequence - http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences
|
||||
return fract(0.5 + f32(index) * vec2<f32>(0.75487766624669276005, 0.5698402909980532659114));
|
||||
}
|
||||
|
||||
fn load_depth(pixel_coordinates: vec2<i32>, mip_level: i32) -> f32 {
|
||||
let mip_size = max(vec2<i32>(uniforms.size) >> vec2<u32>(u32(mip_level)), vec2<i32>(1i));
|
||||
let coordinates = clamp(pixel_coordinates, vec2<i32>(0i), mip_size - 1i);
|
||||
return textureLoad(preprocessed_depth, coordinates, mip_level).r;
|
||||
}
|
||||
|
||||
// Calculate differences in depth between neighbor pixels (later used by the spatial denoiser pass to preserve object edges)
|
||||
fn calculate_neighboring_depth_differences(pixel_coordinates: vec2<i32>) -> f32 {
|
||||
// Sample the pixel's depth and 4 depths around it
|
||||
// PORT: explicit loads instead of two textureGathers.
|
||||
let depth_center = load_depth(pixel_coordinates, 0i);
|
||||
let depth_left = load_depth(pixel_coordinates + vec2<i32>(-1i, 0i), 0i);
|
||||
let depth_top = load_depth(pixel_coordinates + vec2<i32>(0i, -1i), 0i);
|
||||
let depth_bottom = load_depth(pixel_coordinates + vec2<i32>(0i, 1i), 0i);
|
||||
let depth_right = load_depth(pixel_coordinates + vec2<i32>(1i, 0i), 0i);
|
||||
|
||||
// Calculate the depth differences (large differences represent object edges)
|
||||
var edge_info = vec4<f32>(depth_left, depth_right, depth_top, depth_bottom) - depth_center;
|
||||
let slope_left_right = (edge_info.y - edge_info.x) * 0.5;
|
||||
let slope_top_bottom = (edge_info.w - edge_info.z) * 0.5;
|
||||
let edge_info_slope_adjusted = edge_info + vec4<f32>(slope_left_right, -slope_left_right, slope_top_bottom, -slope_top_bottom);
|
||||
edge_info = min(abs(edge_info), abs(edge_info_slope_adjusted));
|
||||
let bias = 0.25; // Using the bias and then saturating nudges the values a bit
|
||||
let scale = depth_center * 0.011; // Weight the edges by their distance from the camera
|
||||
edge_info = saturate((1.0 + bias) - edge_info / scale); // Apply the bias and scale, and invert edge_info so that small values become large, and vice versa
|
||||
|
||||
// Pack the edge info into the texture
|
||||
let edge_info_packed = vec4<u32>(pack4x8unorm(edge_info), 0u, 0u, 0u);
|
||||
textureStore(depth_differences, pixel_coordinates, edge_info_packed);
|
||||
|
||||
return depth_center;
|
||||
}
|
||||
|
||||
fn reconstruct_view_space_position(depth: f32, uv: vec2<f32>) -> vec3<f32> {
|
||||
let clip_xy = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - 2.0 * uv.y);
|
||||
let t = uniforms.inverse_projection * vec4<f32>(clip_xy, depth, 1.0);
|
||||
let view_xyz = t.xyz / t.w;
|
||||
return view_xyz;
|
||||
}
|
||||
|
||||
fn view_position_at(pixel_coordinates: vec2<i32>) -> vec3<f32> {
|
||||
let depth = load_depth(pixel_coordinates, 0i);
|
||||
let uv = (vec2<f32>(pixel_coordinates) + 0.5) * uniforms.inv_size;
|
||||
return reconstruct_view_space_position(depth, uv);
|
||||
}
|
||||
|
||||
// PORT: replaces Bevy's load_normal_view_space (which reads a prepass normal texture we do
|
||||
// not have). Accurate view-space normal reconstruction from depth, atyuwen's 5-tap method:
|
||||
// for each axis, extrapolate the center depth from the two taps on each side and derive the
|
||||
// tangent from whichever side predicts it better. This keeps normals stable across depth
|
||||
// discontinuities where naive derivatives smear.
|
||||
fn reconstruct_normal(pixel_coordinates: vec2<i32>, pixel_position: vec3<f32>, depth_center: f32) -> vec3<f32> {
|
||||
let depth_left1 = load_depth(pixel_coordinates + vec2<i32>(-1i, 0i), 0i);
|
||||
let depth_left2 = load_depth(pixel_coordinates + vec2<i32>(-2i, 0i), 0i);
|
||||
let depth_right1 = load_depth(pixel_coordinates + vec2<i32>(1i, 0i), 0i);
|
||||
let depth_right2 = load_depth(pixel_coordinates + vec2<i32>(2i, 0i), 0i);
|
||||
let depth_top1 = load_depth(pixel_coordinates + vec2<i32>(0i, -1i), 0i);
|
||||
let depth_top2 = load_depth(pixel_coordinates + vec2<i32>(0i, -2i), 0i);
|
||||
let depth_bottom1 = load_depth(pixel_coordinates + vec2<i32>(0i, 1i), 0i);
|
||||
let depth_bottom2 = load_depth(pixel_coordinates + vec2<i32>(0i, 2i), 0i);
|
||||
|
||||
let use_left = abs(2.0 * depth_left1 - depth_left2 - depth_center) <
|
||||
abs(2.0 * depth_right1 - depth_right2 - depth_center);
|
||||
let use_top = abs(2.0 * depth_top1 - depth_top2 - depth_center) <
|
||||
abs(2.0 * depth_bottom1 - depth_bottom2 - depth_center);
|
||||
|
||||
var ddx: vec3<f32>;
|
||||
if use_left {
|
||||
ddx = pixel_position - view_position_at(pixel_coordinates + vec2<i32>(-1i, 0i));
|
||||
} else {
|
||||
ddx = view_position_at(pixel_coordinates + vec2<i32>(1i, 0i)) - pixel_position;
|
||||
}
|
||||
var ddy: vec3<f32>;
|
||||
if use_top {
|
||||
ddy = pixel_position - view_position_at(pixel_coordinates + vec2<i32>(0i, -1i));
|
||||
} else {
|
||||
ddy = view_position_at(pixel_coordinates + vec2<i32>(0i, 1i)) - pixel_position;
|
||||
}
|
||||
|
||||
var normal = normalize(cross(ddy, ddx));
|
||||
// Guard the orientation: the normal must face the camera.
|
||||
if dot(normal, pixel_position) > 0.0 {
|
||||
normal = -normal;
|
||||
}
|
||||
return normal;
|
||||
}
|
||||
|
||||
fn load_and_reconstruct_view_space_position(uv: vec2<f32>, sample_mip_level: f32) -> vec3<f32> {
|
||||
// PORT: point-sample the selected mip explicitly instead of textureSampleLevel.
|
||||
let mip_level = i32(sample_mip_level + 0.5);
|
||||
let mip_size = max(vec2<i32>(uniforms.size) >> vec2<u32>(u32(mip_level)), vec2<i32>(1i));
|
||||
let depth = load_depth(vec2<i32>(uv * vec2<f32>(mip_size)), mip_level);
|
||||
return reconstruct_view_space_position(depth, uv);
|
||||
}
|
||||
|
||||
@compute
|
||||
@workgroup_size(8, 8, 1)
|
||||
fn gtao(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
let slice_count = uniforms.slice_count;
|
||||
let samples_per_slice_side = uniforms.samples_per_slice_side;
|
||||
let effect_radius = uniforms.effect_radius;
|
||||
let falloff_range = 0.615 * effect_radius;
|
||||
let falloff_from = effect_radius * (1.0 - 0.615);
|
||||
let falloff_mul = -1.0 / falloff_range;
|
||||
let falloff_add = falloff_from / falloff_range + 1.0;
|
||||
|
||||
let pixel_coordinates = vec2<i32>(global_id.xy);
|
||||
let uv = (vec2<f32>(pixel_coordinates) + 0.5) * uniforms.inv_size;
|
||||
|
||||
var pixel_depth = calculate_neighboring_depth_differences(pixel_coordinates);
|
||||
let raw_depth = pixel_depth;
|
||||
pixel_depth += 0.00001; // Avoid depth precision issues
|
||||
|
||||
let pixel_position = reconstruct_view_space_position(pixel_depth, uv);
|
||||
// PORT: the reconstruction differences the center position against neighbor positions
|
||||
// built from unbiased depths, so its center must use the raw depth too: at this game's
|
||||
// depth scale (far plane 200000 -> depth ~5e-3) Bevy's +0.00001 bias is comparable to a
|
||||
// one-pixel depth step, and a biased center corrupts both tangents.
|
||||
let pixel_normal = reconstruct_normal(
|
||||
pixel_coordinates, reconstruct_view_space_position(raw_depth, uv), raw_depth);
|
||||
let view_vec = normalize(-pixel_position);
|
||||
|
||||
let noise = load_noise(pixel_coordinates);
|
||||
let sample_scale = (-0.5 * effect_radius * uniforms.projection[0][0]) / pixel_position.z;
|
||||
|
||||
var visibility = 0.0;
|
||||
for (var slice_t = 0.0; slice_t < slice_count; slice_t += 1.0) {
|
||||
let slice = slice_t + noise.x;
|
||||
let phi = (PI / slice_count) * slice;
|
||||
let omega = vec2<f32>(cos(phi), sin(phi));
|
||||
|
||||
let direction = vec3<f32>(omega.xy, 0.0);
|
||||
let orthographic_direction = direction - (dot(direction, view_vec) * view_vec);
|
||||
let axis = cross(direction, view_vec);
|
||||
let projected_normal = pixel_normal - axis * dot(pixel_normal, axis);
|
||||
let projected_normal_length = length(projected_normal);
|
||||
|
||||
let sign_norm = sign(dot(orthographic_direction, projected_normal));
|
||||
let cos_norm = saturate(dot(projected_normal, view_vec) / projected_normal_length);
|
||||
let n = sign_norm * fast_acos(cos_norm);
|
||||
|
||||
let min_cos_horizon_1 = cos(n + HALF_PI);
|
||||
let min_cos_horizon_2 = cos(n - HALF_PI);
|
||||
var cos_horizon_1 = min_cos_horizon_1;
|
||||
var cos_horizon_2 = min_cos_horizon_2;
|
||||
let sample_mul = vec2<f32>(omega.x, -omega.y) * sample_scale;
|
||||
for (var sample_t = 0.0; sample_t < samples_per_slice_side; sample_t += 1.0) {
|
||||
var sample_noise = (slice_t + sample_t * samples_per_slice_side) * 0.6180339887498948482;
|
||||
sample_noise = fract(noise.y + sample_noise);
|
||||
|
||||
var s = (sample_t + sample_noise) / samples_per_slice_side;
|
||||
s *= s; // https://github.com/GameTechDev/XeGTAO#sample-distribution
|
||||
let sample = s * sample_mul;
|
||||
|
||||
// * uniforms.size gets us from [0, 1] to [0, viewport_size], which is needed for this to get the correct mip levels
|
||||
let sample_mip_level = clamp(log2(length(sample * uniforms.size)) - 3.3, 0.0, 4.0); // https://github.com/GameTechDev/XeGTAO#memory-bandwidth-bottleneck
|
||||
let sample_position_1 = load_and_reconstruct_view_space_position(uv + sample, sample_mip_level);
|
||||
let sample_position_2 = load_and_reconstruct_view_space_position(uv - sample, sample_mip_level);
|
||||
|
||||
let sample_difference_1 = sample_position_1 - pixel_position;
|
||||
let sample_difference_2 = sample_position_2 - pixel_position;
|
||||
let sample_distance_1 = length(sample_difference_1);
|
||||
let sample_distance_2 = length(sample_difference_2);
|
||||
var sample_cos_horizon_1 = dot(sample_difference_1 / sample_distance_1, view_vec);
|
||||
var sample_cos_horizon_2 = dot(sample_difference_2 / sample_distance_2, view_vec);
|
||||
|
||||
let weight_1 = saturate(sample_distance_1 * falloff_mul + falloff_add);
|
||||
let weight_2 = saturate(sample_distance_2 * falloff_mul + falloff_add);
|
||||
sample_cos_horizon_1 = mix(min_cos_horizon_1, sample_cos_horizon_1, weight_1);
|
||||
sample_cos_horizon_2 = mix(min_cos_horizon_2, sample_cos_horizon_2, weight_2);
|
||||
|
||||
cos_horizon_1 = max(cos_horizon_1, sample_cos_horizon_1);
|
||||
cos_horizon_2 = max(cos_horizon_2, sample_cos_horizon_2);
|
||||
}
|
||||
|
||||
let horizon_1 = fast_acos(cos_horizon_1);
|
||||
let horizon_2 = -fast_acos(cos_horizon_2);
|
||||
let v1 = (cos_norm + 2.0 * horizon_1 * sin(n) - cos(2.0 * horizon_1 - n)) / 4.0;
|
||||
let v2 = (cos_norm + 2.0 * horizon_2 * sin(n) - cos(2.0 * horizon_2 - n)) / 4.0;
|
||||
visibility += projected_normal_length * (v1 + v2);
|
||||
}
|
||||
visibility /= slice_count;
|
||||
visibility = clamp(visibility, 0.03, 1.0);
|
||||
|
||||
textureStore(ambient_occlusion, pixel_coordinates, vec4<f32>(visibility, 0.0, 0.0, 0.0));
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,19 @@
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2016-2021, Intel Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,138 @@
|
||||
// Inputs a depth texture and outputs a MIP-chain of depths.
|
||||
//
|
||||
// Because SSAO's performance is bound by texture reads, this increases
|
||||
// performance over using the full resolution depth for every sample.
|
||||
//
|
||||
// Reference: https://research.nvidia.com/sites/default/files/pubs/2012-06_Scalable-Ambient-Obscurance/McGuire12SAO.pdf, section 2.2
|
||||
//
|
||||
// Ported from Bevy Engine, crates/bevy_pbr/src/ssao/preprocess_depth.wgsl (v0.13.2),
|
||||
// licensed MIT OR Apache-2.0 (see res/licenses/), itself derived from Intel XeGTAO (MIT).
|
||||
//
|
||||
// PORT: sampler-based gathers replaced with textureLoad (r32float is not filterable without
|
||||
// optional device features), Bevy view uniforms replaced with the mod's own uniform block,
|
||||
// storage format r16float -> r32float (core WebGPU storage format). MIP 4 moved into its own
|
||||
// entry point (core WebGPU limit is 4 storage textures per stage).
|
||||
|
||||
struct Uniforms {
|
||||
projection: mat4x4f,
|
||||
inverse_projection: mat4x4f,
|
||||
size: vec2f, // AO chain size in pixels (MIP 0 of the preprocessed depth)
|
||||
inv_size: vec2f,
|
||||
depth_scale: vec2f, // input depth snapshot pixels per chain pixel (1 or 2)
|
||||
effect_radius: f32, // view-space units
|
||||
intensity: f32,
|
||||
slice_count: f32,
|
||||
samples_per_slice_side: f32,
|
||||
debug_view: u32,
|
||||
_pad: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var input_depth: texture_2d<f32>;
|
||||
@group(0) @binding(1) var preprocessed_depth_mip0: texture_storage_2d<r32float, write>;
|
||||
@group(0) @binding(2) var preprocessed_depth_mip1: texture_storage_2d<r32float, write>;
|
||||
@group(0) @binding(3) var preprocessed_depth_mip2: texture_storage_2d<r32float, write>;
|
||||
@group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d<r32float, write>;
|
||||
@group(0) @binding(5) var<uniform> uniforms: Uniforms;
|
||||
// downsample_mip4 entry point only (disjoint subresources of the same texture).
|
||||
@group(0) @binding(6) var preprocessed_depth_mip3_in: texture_2d<f32>;
|
||||
@group(0) @binding(7) var preprocessed_depth_mip4: texture_storage_2d<r32float, write>;
|
||||
|
||||
// PORT: replaces the textureGather of the input depth with explicit loads (also handles the
|
||||
// half-resolution case, where one chain texel covers depth_scale snapshot texels).
|
||||
fn load_input_depth(pixel_coordinates: vec2<i32>) -> f32 {
|
||||
let input_size = vec2<i32>(uniforms.size * uniforms.depth_scale);
|
||||
let coordinates = clamp(vec2<i32>(vec2<f32>(pixel_coordinates) * uniforms.depth_scale),
|
||||
vec2<i32>(0i), input_size - 1i);
|
||||
return textureLoad(input_depth, coordinates, 0i).r;
|
||||
}
|
||||
|
||||
// Using 4 depths from the previous MIP, compute a weighted average for the depth of the current MIP
|
||||
fn weighted_average(depth0: f32, depth1: f32, depth2: f32, depth3: f32) -> f32 {
|
||||
let depth_range_scale_factor = 0.75;
|
||||
let effect_radius = depth_range_scale_factor * 0.5 * 1.457;
|
||||
let falloff_range = 0.615 * effect_radius;
|
||||
let falloff_from = effect_radius * (1.0 - 0.615);
|
||||
let falloff_mul = -1.0 / falloff_range;
|
||||
let falloff_add = falloff_from / falloff_range + 1.0;
|
||||
|
||||
let min_depth = min(min(depth0, depth1), min(depth2, depth3));
|
||||
let weight0 = saturate((depth0 - min_depth) * falloff_mul + falloff_add);
|
||||
let weight1 = saturate((depth1 - min_depth) * falloff_mul + falloff_add);
|
||||
let weight2 = saturate((depth2 - min_depth) * falloff_mul + falloff_add);
|
||||
let weight3 = saturate((depth3 - min_depth) * falloff_mul + falloff_add);
|
||||
let weight_total = weight0 + weight1 + weight2 + weight3;
|
||||
|
||||
return ((weight0 * depth0) + (weight1 * depth1) + (weight2 * depth2) + (weight3 * depth3)) / weight_total;
|
||||
}
|
||||
|
||||
// Used to share the depths from the previous MIP level between all invocations in a workgroup
|
||||
var<workgroup> previous_mip_depth: array<array<f32, 8>, 8>;
|
||||
|
||||
@compute
|
||||
@workgroup_size(8, 8, 1)
|
||||
fn preprocess_depth(@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>) {
|
||||
let base_coordinates = vec2<i32>(global_id.xy);
|
||||
|
||||
// MIP 0 - Copy 4 texels from the input depth (per invocation, 8x8 invocations per workgroup)
|
||||
let pixel_coordinates0 = base_coordinates * 2i;
|
||||
let pixel_coordinates1 = pixel_coordinates0 + vec2<i32>(1i, 0i);
|
||||
let pixel_coordinates2 = pixel_coordinates0 + vec2<i32>(0i, 1i);
|
||||
let pixel_coordinates3 = pixel_coordinates0 + vec2<i32>(1i, 1i);
|
||||
let depth0 = load_input_depth(pixel_coordinates0);
|
||||
let depth1 = load_input_depth(pixel_coordinates1);
|
||||
let depth2 = load_input_depth(pixel_coordinates2);
|
||||
let depth3 = load_input_depth(pixel_coordinates3);
|
||||
textureStore(preprocessed_depth_mip0, pixel_coordinates0, vec4<f32>(depth0, 0.0, 0.0, 0.0));
|
||||
textureStore(preprocessed_depth_mip0, pixel_coordinates1, vec4<f32>(depth1, 0.0, 0.0, 0.0));
|
||||
textureStore(preprocessed_depth_mip0, pixel_coordinates2, vec4<f32>(depth2, 0.0, 0.0, 0.0));
|
||||
textureStore(preprocessed_depth_mip0, pixel_coordinates3, vec4<f32>(depth3, 0.0, 0.0, 0.0));
|
||||
|
||||
// MIP 1 - Weighted average of MIP 0's depth values (per invocation, 8x8 invocations per workgroup)
|
||||
let depth_mip1 = weighted_average(depth0, depth1, depth2, depth3);
|
||||
textureStore(preprocessed_depth_mip1, base_coordinates, vec4<f32>(depth_mip1, 0.0, 0.0, 0.0));
|
||||
previous_mip_depth[local_id.x][local_id.y] = depth_mip1;
|
||||
|
||||
workgroupBarrier();
|
||||
|
||||
// MIP 2 - Weighted average of MIP 1's depth values (per invocation, 4x4 invocations per workgroup)
|
||||
if all(local_id.xy % vec2<u32>(2u) == vec2<u32>(0u)) {
|
||||
let mip2_depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u];
|
||||
let mip2_depth1 = previous_mip_depth[local_id.x + 1u][local_id.y + 0u];
|
||||
let mip2_depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 1u];
|
||||
let mip2_depth3 = previous_mip_depth[local_id.x + 1u][local_id.y + 1u];
|
||||
let depth_mip2 = weighted_average(mip2_depth0, mip2_depth1, mip2_depth2, mip2_depth3);
|
||||
textureStore(preprocessed_depth_mip2, base_coordinates / 2i, vec4<f32>(depth_mip2, 0.0, 0.0, 0.0));
|
||||
previous_mip_depth[local_id.x][local_id.y] = depth_mip2;
|
||||
}
|
||||
|
||||
workgroupBarrier();
|
||||
|
||||
// MIP 3 - Weighted average of MIP 2's depth values (per invocation, 2x2 invocations per workgroup)
|
||||
if all(local_id.xy % vec2<u32>(4u) == vec2<u32>(0u)) {
|
||||
let mip3_depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u];
|
||||
let mip3_depth1 = previous_mip_depth[local_id.x + 2u][local_id.y + 0u];
|
||||
let mip3_depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 2u];
|
||||
let mip3_depth3 = previous_mip_depth[local_id.x + 2u][local_id.y + 2u];
|
||||
let depth_mip3 = weighted_average(mip3_depth0, mip3_depth1, mip3_depth2, mip3_depth3);
|
||||
textureStore(preprocessed_depth_mip3, base_coordinates / 4i, vec4<f32>(depth_mip3, 0.0, 0.0, 0.0));
|
||||
previous_mip_depth[local_id.x][local_id.y] = depth_mip3;
|
||||
}
|
||||
}
|
||||
|
||||
// MIP 4: weighted average of MIP 3's depth values, as a second (tiny) dispatch.
|
||||
@compute
|
||||
@workgroup_size(8, 8, 1)
|
||||
fn downsample_mip4(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
let base_coordinates = vec2<i32>(global_id.xy);
|
||||
let mip3_size = max(vec2<i32>(textureDimensions(preprocessed_depth_mip3_in)), vec2<i32>(1i));
|
||||
let coordinates0 = clamp(base_coordinates * 2i, vec2<i32>(0i), mip3_size - 1i);
|
||||
let coordinates1 = clamp(base_coordinates * 2i + vec2<i32>(1i, 0i), vec2<i32>(0i), mip3_size - 1i);
|
||||
let coordinates2 = clamp(base_coordinates * 2i + vec2<i32>(0i, 1i), vec2<i32>(0i), mip3_size - 1i);
|
||||
let coordinates3 = clamp(base_coordinates * 2i + vec2<i32>(1i, 1i), vec2<i32>(0i), mip3_size - 1i);
|
||||
let depth0 = textureLoad(preprocessed_depth_mip3_in, coordinates0, 0i).r;
|
||||
let depth1 = textureLoad(preprocessed_depth_mip3_in, coordinates1, 0i).r;
|
||||
let depth2 = textureLoad(preprocessed_depth_mip3_in, coordinates2, 0i).r;
|
||||
let depth3 = textureLoad(preprocessed_depth_mip3_in, coordinates3, 0i).r;
|
||||
let depth_mip4 = weighted_average(depth0, depth1, depth2, depth3);
|
||||
textureStore(preprocessed_depth_mip4, base_coordinates, vec4<f32>(depth_mip4, 0.0, 0.0, 0.0));
|
||||
}
|
||||
@@ -0,0 +1,931 @@
|
||||
// Ambient occlusion (GTAO) example mod.
|
||||
//
|
||||
// Showcases the gfx service's compute tasks and the camera service: after opaque scene draws,
|
||||
// before translucent/fog overlays, the scene depth is resolved and a three-dispatch compute
|
||||
// chain (depth MIP prefilter, GTAO, spatial denoise) produces a visibility texture that a
|
||||
// fullscreen draw multiplies over the world.
|
||||
//
|
||||
// The WGSL in res/ is ported from Bevy Engine's SSAO implementation (MIT OR Apache-2.0),
|
||||
// itself based on Intel XeGTAO (MIT); see res/licenses/ and the `PORT:` notes in the shaders.
|
||||
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/camera.h"
|
||||
#include "mods/svc/config.h"
|
||||
#include "mods/svc/gfx.h"
|
||||
#include "mods/svc/log.h"
|
||||
#include "mods/svc/resource.h"
|
||||
#include "mods/svc/ui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
DEFINE_MOD();
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
IMPORT_SERVICE(ConfigService, svc_config);
|
||||
IMPORT_SERVICE(ResourceService, svc_resource);
|
||||
IMPORT_SERVICE(UiService, svc_ui);
|
||||
IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
IMPORT_SERVICE(CameraService, svc_camera);
|
||||
|
||||
namespace {
|
||||
|
||||
ConfigVarHandle g_cvarEnabled = 0;
|
||||
ConfigVarHandle g_cvarQuality = 0;
|
||||
ConfigVarHandle g_cvarRadius = 0;
|
||||
ConfigVarHandle g_cvarIntensity = 0;
|
||||
ConfigVarHandle g_cvarHalfRes = 0;
|
||||
ConfigVarHandle g_cvarDebugView = 0;
|
||||
|
||||
GfxComputeTypeHandle g_computeType = 0;
|
||||
GfxDrawTypeHandle g_drawType = 0;
|
||||
GfxStageHookHandle g_afterOpaqueHook = 0;
|
||||
UiWindowHandle g_controlsWindow = 0;
|
||||
|
||||
ResourceBuffer g_preprocessSource = RESOURCE_BUFFER_INIT;
|
||||
ResourceBuffer g_gtaoSource = RESOURCE_BUFFER_INIT;
|
||||
ResourceBuffer g_denoiseSource = RESOURCE_BUFFER_INIT;
|
||||
ResourceBuffer g_compositeSource = RESOURCE_BUFFER_INIT;
|
||||
|
||||
GfxDeviceInfo g_deviceInfo = GFX_DEVICE_INFO_INIT;
|
||||
WGPUComputePipeline g_preprocessPipeline = nullptr;
|
||||
WGPUComputePipeline g_mip4Pipeline = nullptr;
|
||||
WGPUComputePipeline g_gtaoPipeline = nullptr;
|
||||
WGPUComputePipeline g_denoisePipeline = nullptr;
|
||||
WGPUBindGroupLayout g_preprocessLayout = nullptr;
|
||||
WGPUBindGroupLayout g_mip4Layout = nullptr;
|
||||
WGPUBindGroupLayout g_gtaoLayout = nullptr;
|
||||
WGPUBindGroupLayout g_denoiseLayout = nullptr;
|
||||
WGPURenderPipeline g_compositePipeline = nullptr;
|
||||
WGPURenderPipeline g_compositeDebugPipeline = nullptr;
|
||||
WGPUBindGroupLayout g_compositeLayout = nullptr;
|
||||
WGPUBindGroupLayout g_compositeDebugLayout = nullptr;
|
||||
WGPUTexture g_hilbertLut = nullptr;
|
||||
WGPUTextureView g_hilbertLutView = nullptr;
|
||||
|
||||
// AO chain targets, recreated when the render size (or halfRes) changes. Old sets are retired
|
||||
// for a few frames instead of released immediately: payloads embedding their views may still
|
||||
// be in flight on the render worker.
|
||||
struct AoTargets {
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
WGPUTexture preprocessedDepth = nullptr;
|
||||
WGPUTextureView preprocessedDepthMips[5] = {};
|
||||
WGPUTextureView preprocessedDepthAll = nullptr;
|
||||
WGPUTexture aoNoisy = nullptr;
|
||||
WGPUTextureView aoNoisyView = nullptr;
|
||||
WGPUTexture depthDifferences = nullptr;
|
||||
WGPUTextureView depthDifferencesView = nullptr;
|
||||
WGPUTexture aoFinal = nullptr;
|
||||
WGPUTextureView aoFinalView = nullptr;
|
||||
};
|
||||
AoTargets g_targets;
|
||||
struct RetiredTargets {
|
||||
AoTargets targets;
|
||||
int framesLeft = 0;
|
||||
};
|
||||
std::vector<RetiredTargets> g_retiredTargets;
|
||||
|
||||
bool g_warnedNoDepth = false;
|
||||
bool g_loggedChain = false;
|
||||
std::atomic g_chainExecuted{false};
|
||||
|
||||
// Mirror of the WGSL Uniforms struct (keep in sync with res/*.wgsl).
|
||||
struct AoUniforms {
|
||||
float projection[16];
|
||||
float inverse_projection[16];
|
||||
float size[2];
|
||||
float inv_size[2];
|
||||
float depth_scale[2];
|
||||
float effect_radius;
|
||||
float intensity;
|
||||
float slice_count;
|
||||
float samples_per_slice_side;
|
||||
uint32_t debug_view;
|
||||
float _pad;
|
||||
};
|
||||
static_assert(sizeof(AoUniforms) % 16 == 0);
|
||||
|
||||
struct ComputePayload {
|
||||
WGPUTextureView depth; // frame-pooled scene depth snapshot
|
||||
WGPUTextureView preprocessedDepthMips[5];
|
||||
WGPUTextureView preprocessedDepthAll;
|
||||
WGPUTextureView aoNoisy;
|
||||
WGPUTextureView depthDifferences;
|
||||
WGPUTextureView aoFinal;
|
||||
uint32_t uniform_offset;
|
||||
uint32_t uniform_size;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
};
|
||||
static_assert(sizeof(ComputePayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE);
|
||||
static_assert(std::is_trivially_copyable_v<ComputePayload>);
|
||||
|
||||
struct CompositePayload {
|
||||
WGPUTextureView aoFinal;
|
||||
WGPUTextureView preprocessedDepth; // debug views reconstruct normals/depth from it
|
||||
WGPUTextureView sceneDepth; // raw snapshot, for the bypass debug views
|
||||
uint32_t uniform_offset;
|
||||
uint32_t uniform_size;
|
||||
uint32_t debug_view;
|
||||
};
|
||||
static_assert(sizeof(CompositePayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE);
|
||||
static_assert(std::is_trivially_copyable_v<CompositePayload>);
|
||||
|
||||
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback) {
|
||||
int64_t value = fallback;
|
||||
if (handle == 0 || svc_config->get_int(mod_ctx, handle, &value) != MOD_OK) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool get_bool_option(ConfigVarHandle handle, bool fallback) {
|
||||
bool value = fallback;
|
||||
if (handle == 0 || svc_config->get_bool(mod_ctx, handle, &value) != MOD_OK) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// XeGTAO/Bevy quality presets: slices x (samples per slice side * 2).
|
||||
void quality_counts(int64_t quality, float& sliceCount, float& samplesPerSliceSide) {
|
||||
switch (std::clamp<int64_t>(quality, 0, 3)) {
|
||||
case 0:
|
||||
sliceCount = 1.0f;
|
||||
samplesPerSliceSide = 2.0f;
|
||||
break;
|
||||
case 1:
|
||||
sliceCount = 2.0f;
|
||||
samplesPerSliceSide = 2.0f;
|
||||
break;
|
||||
default:
|
||||
case 2:
|
||||
sliceCount = 3.0f;
|
||||
samplesPerSliceSide = 3.0f;
|
||||
break;
|
||||
case 3:
|
||||
sliceCount = 9.0f;
|
||||
samplesPerSliceSide = 3.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WGPUShaderModule create_shader_module(const char* label, const ResourceBuffer& source) {
|
||||
WGPUShaderSourceWGSL wgsl = WGPU_SHADER_SOURCE_WGSL_INIT;
|
||||
wgsl.code = {static_cast<const char*>(source.data), source.size};
|
||||
WGPUShaderModuleDescriptor moduleDesc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT;
|
||||
moduleDesc.nextInChain = &wgsl.chain;
|
||||
moduleDesc.label = {label, WGPU_STRLEN};
|
||||
return wgpuDeviceCreateShaderModule(g_deviceInfo.device, &moduleDesc);
|
||||
}
|
||||
|
||||
bool build_compute_pipeline(const char* label, const ResourceBuffer& source, const char* entry,
|
||||
WGPUComputePipeline& outPipeline, WGPUBindGroupLayout& outLayout) {
|
||||
WGPUShaderModule module = create_shader_module(label, source);
|
||||
if (module == nullptr) {
|
||||
return false;
|
||||
}
|
||||
WGPUComputePipelineDescriptor pipelineDesc = WGPU_COMPUTE_PIPELINE_DESCRIPTOR_INIT;
|
||||
pipelineDesc.label = {label, WGPU_STRLEN};
|
||||
pipelineDesc.compute.module = module;
|
||||
pipelineDesc.compute.entryPoint = {entry, WGPU_STRLEN};
|
||||
outPipeline = wgpuDeviceCreateComputePipeline(g_deviceInfo.device, &pipelineDesc);
|
||||
wgpuShaderModuleRelease(module);
|
||||
if (outPipeline == nullptr) {
|
||||
return false;
|
||||
}
|
||||
outLayout = wgpuComputePipelineGetBindGroupLayout(outPipeline, 0);
|
||||
return outLayout != nullptr;
|
||||
}
|
||||
|
||||
bool build_composite_pipeline(
|
||||
bool blend, WGPURenderPipeline& outPipeline, WGPUBindGroupLayout& outLayout) {
|
||||
WGPUShaderModule module = create_shader_module("AO composite", g_compositeSource);
|
||||
if (module == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Multiply blend
|
||||
WGPUBlendState blendState{
|
||||
.color =
|
||||
{
|
||||
.operation = WGPUBlendOperation_Add,
|
||||
.srcFactor = WGPUBlendFactor_Dst,
|
||||
.dstFactor = WGPUBlendFactor_Zero,
|
||||
},
|
||||
.alpha =
|
||||
{
|
||||
.operation = WGPUBlendOperation_Add,
|
||||
.srcFactor = WGPUBlendFactor_Zero,
|
||||
.dstFactor = WGPUBlendFactor_One,
|
||||
},
|
||||
};
|
||||
WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT;
|
||||
colorTarget.format = g_deviceInfo.color_format;
|
||||
if (blend) {
|
||||
colorTarget.blend = &blendState;
|
||||
}
|
||||
WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT;
|
||||
fragment.module = module;
|
||||
fragment.entryPoint = {"fs_main", WGPU_STRLEN};
|
||||
fragment.targetCount = 1;
|
||||
fragment.targets = &colorTarget;
|
||||
// Depth state must match the EFB pass despite never touching depth.
|
||||
WGPUDepthStencilState depthStencil = WGPU_DEPTH_STENCIL_STATE_INIT;
|
||||
depthStencil.format = g_deviceInfo.depth_format;
|
||||
depthStencil.depthWriteEnabled = WGPUOptionalBool_False;
|
||||
depthStencil.depthCompare = WGPUCompareFunction_Always;
|
||||
|
||||
WGPURenderPipelineDescriptor pipelineDesc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT;
|
||||
pipelineDesc.label = {blend ? "AO composite" : "AO composite (debug)", WGPU_STRLEN};
|
||||
pipelineDesc.vertex.module = module;
|
||||
pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN};
|
||||
pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
pipelineDesc.depthStencil = &depthStencil;
|
||||
pipelineDesc.multisample.count = g_deviceInfo.sample_count;
|
||||
pipelineDesc.fragment = &fragment;
|
||||
outPipeline = wgpuDeviceCreateRenderPipeline(g_deviceInfo.device, &pipelineDesc);
|
||||
wgpuShaderModuleRelease(module);
|
||||
if (outPipeline == nullptr) {
|
||||
return false;
|
||||
}
|
||||
outLayout = wgpuRenderPipelineGetBindGroupLayout(outPipeline, 0);
|
||||
return outLayout != nullptr;
|
||||
}
|
||||
|
||||
// Hilbert curve index LUT for the R2 noise sequence, generated once at init.
|
||||
// Ported from Bevy's generate_hilbert_index_lut (https://www.shadertoy.com/view/3tB3z3).
|
||||
uint16_t hilbert_index(uint16_t x, uint16_t y) {
|
||||
uint16_t index = 0;
|
||||
for (uint16_t level = 32; level > 0; level /= 2) {
|
||||
const uint16_t regionX = (x & level) > 0 ? 1 : 0;
|
||||
const uint16_t regionY = (y & level) > 0 ? 1 : 0;
|
||||
index += level * level * ((3 * regionX) ^ regionY);
|
||||
if (regionY == 0) {
|
||||
if (regionX == 1) {
|
||||
x = 63 - x;
|
||||
y = 63 - y;
|
||||
}
|
||||
std::swap(x, y);
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
bool build_hilbert_lut() {
|
||||
WGPUTextureDescriptor texDesc = WGPU_TEXTURE_DESCRIPTOR_INIT;
|
||||
texDesc.label = {"AO hilbert LUT", WGPU_STRLEN};
|
||||
texDesc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst;
|
||||
texDesc.size = {64, 64, 1};
|
||||
texDesc.format = WGPUTextureFormat_R16Uint;
|
||||
g_hilbertLut = wgpuDeviceCreateTexture(g_deviceInfo.device, &texDesc);
|
||||
if (g_hilbertLut == nullptr) {
|
||||
return false;
|
||||
}
|
||||
g_hilbertLutView = wgpuTextureCreateView(g_hilbertLut, nullptr);
|
||||
if (g_hilbertLutView == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t lut[64 * 64];
|
||||
for (uint16_t y = 0; y < 64; ++y) {
|
||||
for (uint16_t x = 0; x < 64; ++x) {
|
||||
lut[y * 64 + x] = hilbert_index(x, y);
|
||||
}
|
||||
}
|
||||
WGPUTexelCopyTextureInfo dst = WGPU_TEXEL_COPY_TEXTURE_INFO_INIT;
|
||||
dst.texture = g_hilbertLut;
|
||||
WGPUTexelCopyBufferLayout layout{.offset = 0, .bytesPerRow = 64 * 2, .rowsPerImage = 64};
|
||||
WGPUExtent3D extent{64, 64, 1};
|
||||
wgpuQueueWriteTexture(g_deviceInfo.queue, &dst, lut, sizeof(lut), &layout, &extent);
|
||||
return true;
|
||||
}
|
||||
|
||||
void release_targets(AoTargets& targets) {
|
||||
for (auto*& view : targets.preprocessedDepthMips) {
|
||||
if (view != nullptr) {
|
||||
wgpuTextureViewRelease(view);
|
||||
view = nullptr;
|
||||
}
|
||||
}
|
||||
const auto releaseView = [](WGPUTextureView& view) {
|
||||
if (view != nullptr) {
|
||||
wgpuTextureViewRelease(view);
|
||||
view = nullptr;
|
||||
}
|
||||
};
|
||||
const auto releaseTexture = [](WGPUTexture& texture) {
|
||||
if (texture != nullptr) {
|
||||
wgpuTextureRelease(texture);
|
||||
texture = nullptr;
|
||||
}
|
||||
};
|
||||
releaseView(targets.preprocessedDepthAll);
|
||||
releaseView(targets.aoNoisyView);
|
||||
releaseView(targets.depthDifferencesView);
|
||||
releaseView(targets.aoFinalView);
|
||||
releaseTexture(targets.preprocessedDepth);
|
||||
releaseTexture(targets.aoNoisy);
|
||||
releaseTexture(targets.depthDifferences);
|
||||
releaseTexture(targets.aoFinal);
|
||||
targets.width = targets.height = 0;
|
||||
}
|
||||
|
||||
void tick_retired_targets() {
|
||||
for (auto it = g_retiredTargets.begin(); it != g_retiredTargets.end();) {
|
||||
if (--it->framesLeft <= 0) {
|
||||
release_targets(it->targets);
|
||||
it = g_retiredTargets.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ensure_targets(uint32_t width, uint32_t height) {
|
||||
if (g_targets.width == width && g_targets.height == height) {
|
||||
return true;
|
||||
}
|
||||
if (g_targets.width != 0) {
|
||||
g_retiredTargets.push_back(RetiredTargets{std::exchange(g_targets, AoTargets{}), 4});
|
||||
}
|
||||
|
||||
const auto createStorageTexture = [&](const char* label, WGPUTextureFormat format,
|
||||
uint32_t mipCount, WGPUTexture& outTexture) {
|
||||
WGPUTextureDescriptor texDesc = WGPU_TEXTURE_DESCRIPTOR_INIT;
|
||||
texDesc.label = {label, WGPU_STRLEN};
|
||||
texDesc.usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding;
|
||||
texDesc.size = {width, height, 1};
|
||||
texDesc.format = format;
|
||||
texDesc.mipLevelCount = mipCount;
|
||||
outTexture = wgpuDeviceCreateTexture(g_deviceInfo.device, &texDesc);
|
||||
return outTexture != nullptr;
|
||||
};
|
||||
|
||||
bool ok = createStorageTexture("AO preprocessed depth", WGPUTextureFormat_R32Float, 5,
|
||||
g_targets.preprocessedDepth) &&
|
||||
createStorageTexture("AO noisy", WGPUTextureFormat_R32Float, 1, g_targets.aoNoisy) &&
|
||||
createStorageTexture("AO depth differences", WGPUTextureFormat_R32Uint, 1,
|
||||
g_targets.depthDifferences) &&
|
||||
createStorageTexture("AO final", WGPUTextureFormat_R32Float, 1, g_targets.aoFinal);
|
||||
if (ok) {
|
||||
for (uint32_t mip = 0; mip < 5 && ok; ++mip) {
|
||||
WGPUTextureViewDescriptor viewDesc = WGPU_TEXTURE_VIEW_DESCRIPTOR_INIT;
|
||||
viewDesc.baseMipLevel = mip;
|
||||
viewDesc.mipLevelCount = 1;
|
||||
g_targets.preprocessedDepthMips[mip] =
|
||||
wgpuTextureCreateView(g_targets.preprocessedDepth, &viewDesc);
|
||||
ok = g_targets.preprocessedDepthMips[mip] != nullptr;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
g_targets.preprocessedDepthAll =
|
||||
wgpuTextureCreateView(g_targets.preprocessedDepth, nullptr);
|
||||
g_targets.aoNoisyView = wgpuTextureCreateView(g_targets.aoNoisy, nullptr);
|
||||
g_targets.depthDifferencesView = wgpuTextureCreateView(g_targets.depthDifferences, nullptr);
|
||||
g_targets.aoFinalView = wgpuTextureCreateView(g_targets.aoFinal, nullptr);
|
||||
ok = g_targets.preprocessedDepthAll != nullptr && g_targets.aoNoisyView != nullptr &&
|
||||
g_targets.depthDifferencesView != nullptr && g_targets.aoFinalView != nullptr;
|
||||
}
|
||||
if (!ok) {
|
||||
release_targets(g_targets);
|
||||
return false;
|
||||
}
|
||||
g_targets.width = width;
|
||||
g_targets.height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
constexpr uint32_t div_ceil(uint32_t numerator, uint32_t denominator) {
|
||||
return (numerator + denominator - 1) / denominator;
|
||||
}
|
||||
|
||||
// Render worker thread: the AO chain as one compute pass with three dispatches.
|
||||
void on_compute(
|
||||
ModContext*, const GfxComputeContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
if (payloadSize != sizeof(ComputePayload)) {
|
||||
return;
|
||||
}
|
||||
ComputePayload data;
|
||||
std::memcpy(&data, payload, sizeof(data));
|
||||
if (data.depth == nullptr || g_preprocessPipeline == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto makeBindGroup = [&](WGPUBindGroupLayout layout,
|
||||
std::initializer_list<WGPUBindGroupEntry> entries) {
|
||||
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
|
||||
bindGroupDesc.layout = layout;
|
||||
bindGroupDesc.entryCount = entries.size();
|
||||
bindGroupDesc.entries = entries.begin();
|
||||
return wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
|
||||
};
|
||||
const auto textureEntry = [](uint32_t binding, WGPUTextureView view) {
|
||||
WGPUBindGroupEntry entry = WGPU_BIND_GROUP_ENTRY_INIT;
|
||||
entry.binding = binding;
|
||||
entry.textureView = view;
|
||||
return entry;
|
||||
};
|
||||
const auto uniformEntry = [&](uint32_t binding) {
|
||||
WGPUBindGroupEntry entry = WGPU_BIND_GROUP_ENTRY_INIT;
|
||||
entry.binding = binding;
|
||||
entry.buffer = ctx->uniform_buffer;
|
||||
entry.offset = data.uniform_offset;
|
||||
entry.size = data.uniform_size;
|
||||
return entry;
|
||||
};
|
||||
|
||||
WGPUBindGroup preprocessGroup = makeBindGroup(g_preprocessLayout,
|
||||
{textureEntry(0, data.depth), textureEntry(1, data.preprocessedDepthMips[0]),
|
||||
textureEntry(2, data.preprocessedDepthMips[1]),
|
||||
textureEntry(3, data.preprocessedDepthMips[2]),
|
||||
textureEntry(4, data.preprocessedDepthMips[3]), uniformEntry(5)});
|
||||
WGPUBindGroup mip4Group =
|
||||
makeBindGroup(g_mip4Layout, {textureEntry(6, data.preprocessedDepthMips[3]),
|
||||
textureEntry(7, data.preprocessedDepthMips[4])});
|
||||
WGPUBindGroup gtaoGroup = makeBindGroup(
|
||||
g_gtaoLayout, {textureEntry(0, data.preprocessedDepthAll),
|
||||
textureEntry(1, g_hilbertLutView), textureEntry(2, data.aoNoisy),
|
||||
textureEntry(3, data.depthDifferences), uniformEntry(4)});
|
||||
WGPUBindGroup denoiseGroup = makeBindGroup(
|
||||
g_denoiseLayout, {textureEntry(0, data.aoNoisy), textureEntry(1, data.depthDifferences),
|
||||
textureEntry(2, data.aoFinal), uniformEntry(3)});
|
||||
if (preprocessGroup == nullptr || mip4Group == nullptr || gtaoGroup == nullptr ||
|
||||
denoiseGroup == nullptr)
|
||||
{
|
||||
const auto release = [](WGPUBindGroup group) {
|
||||
if (group != nullptr) {
|
||||
wgpuBindGroupRelease(group);
|
||||
}
|
||||
};
|
||||
release(preprocessGroup);
|
||||
release(mip4Group);
|
||||
release(gtaoGroup);
|
||||
release(denoiseGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
WGPUComputePassDescriptor passDesc = WGPU_COMPUTE_PASS_DESCRIPTOR_INIT;
|
||||
passDesc.label = {"AO chain", WGPU_STRLEN};
|
||||
WGPUComputePassEncoder pass = wgpuCommandEncoderBeginComputePass(ctx->encoder, &passDesc);
|
||||
// Each preprocess workgroup covers 16x16 MIP-0 texels (8x8 invocations, 2x2 texels each).
|
||||
wgpuComputePassEncoderSetPipeline(pass, g_preprocessPipeline);
|
||||
wgpuComputePassEncoderSetBindGroup(pass, 0, preprocessGroup, 0, nullptr);
|
||||
wgpuComputePassEncoderDispatchWorkgroups(
|
||||
pass, div_ceil(data.width, 16), div_ceil(data.height, 16), 1);
|
||||
wgpuComputePassEncoderSetPipeline(pass, g_mip4Pipeline);
|
||||
wgpuComputePassEncoderSetBindGroup(pass, 0, mip4Group, 0, nullptr);
|
||||
wgpuComputePassEncoderDispatchWorkgroups(pass, div_ceil(std::max(data.width >> 4, 1u), 8),
|
||||
div_ceil(std::max(data.height >> 4, 1u), 8), 1);
|
||||
wgpuComputePassEncoderSetPipeline(pass, g_gtaoPipeline);
|
||||
wgpuComputePassEncoderSetBindGroup(pass, 0, gtaoGroup, 0, nullptr);
|
||||
wgpuComputePassEncoderDispatchWorkgroups(
|
||||
pass, div_ceil(data.width, 8), div_ceil(data.height, 8), 1);
|
||||
wgpuComputePassEncoderSetPipeline(pass, g_denoisePipeline);
|
||||
wgpuComputePassEncoderSetBindGroup(pass, 0, denoiseGroup, 0, nullptr);
|
||||
wgpuComputePassEncoderDispatchWorkgroups(
|
||||
pass, div_ceil(data.width, 8), div_ceil(data.height, 8), 1);
|
||||
wgpuComputePassEncoderEnd(pass);
|
||||
wgpuComputePassEncoderRelease(pass);
|
||||
|
||||
wgpuBindGroupRelease(preprocessGroup);
|
||||
wgpuBindGroupRelease(mip4Group);
|
||||
wgpuBindGroupRelease(gtaoGroup);
|
||||
wgpuBindGroupRelease(denoiseGroup);
|
||||
g_chainExecuted.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Render worker thread: composite the AO over the scene (or show it, in debug view).
|
||||
void on_draw(
|
||||
ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
if (payloadSize != sizeof(CompositePayload)) {
|
||||
return;
|
||||
}
|
||||
CompositePayload data;
|
||||
std::memcpy(&data, payload, sizeof(data));
|
||||
WGPURenderPipeline pipeline =
|
||||
data.debug_view != 0 ? g_compositeDebugPipeline : g_compositePipeline;
|
||||
WGPUBindGroupLayout layout = data.debug_view != 0 ? g_compositeDebugLayout : g_compositeLayout;
|
||||
if (data.aoFinal == nullptr || data.preprocessedDepth == nullptr ||
|
||||
data.sceneDepth == nullptr || pipeline == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
|
||||
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
|
||||
entries[0].binding = 0;
|
||||
entries[0].textureView = data.aoFinal;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = data.preprocessedDepth;
|
||||
entries[2].binding = 2;
|
||||
entries[2].textureView = data.sceneDepth;
|
||||
entries[3].binding = 3;
|
||||
entries[3].buffer = ctx->uniform_buffer;
|
||||
entries[3].offset = data.uniform_offset;
|
||||
entries[3].size = data.uniform_size;
|
||||
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
|
||||
bindGroupDesc.layout = layout;
|
||||
bindGroupDesc.entryCount = 4;
|
||||
bindGroupDesc.entries = entries;
|
||||
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
|
||||
if (bindGroup == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
wgpuRenderPassEncoderSetPipeline(ctx->pass, pipeline);
|
||||
wgpuRenderPassEncoderSetBindGroup(ctx->pass, 0, bindGroup, 0, nullptr);
|
||||
wgpuRenderPassEncoderDraw(ctx->pass, 3, 1, 0, 0);
|
||||
wgpuBindGroupRelease(bindGroup);
|
||||
}
|
||||
|
||||
// Game thread, after opaque scene draws and before translucent/fog overlay lists.
|
||||
void on_scene_after_opaque(ModContext*, const GfxStageContext* stageCtx, void*) {
|
||||
tick_retired_targets();
|
||||
if (!get_bool_option(g_cvarEnabled, true)) {
|
||||
return;
|
||||
}
|
||||
if (stageCtx == nullptr || stageCtx->struct_size < sizeof(GfxStageContext) ||
|
||||
stageCtx->game_view == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CameraInfo camera = CAMERA_INFO_INIT;
|
||||
if (svc_camera->get_camera(mod_ctx, stageCtx->game_view, &camera) != MOD_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
GfxResolveDesc resolveDesc = GFX_RESOLVE_DESC_INIT;
|
||||
resolveDesc.color = false;
|
||||
resolveDesc.depth = true;
|
||||
GfxResolvedTargets resolved = GFX_RESOLVED_TARGETS_INIT;
|
||||
if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK ||
|
||||
resolved.depth == nullptr)
|
||||
{
|
||||
if (!g_warnedNoDepth) {
|
||||
g_warnedNoDepth = true;
|
||||
svc_log->warn(mod_ctx, "depth snapshots unavailable; AO disabled");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bool halfRes = get_bool_option(g_cvarHalfRes, false);
|
||||
const uint32_t divisor = halfRes ? 2 : 1;
|
||||
const uint32_t width = resolved.width / divisor;
|
||||
const uint32_t height = resolved.height / divisor;
|
||||
if (width < 32 || height < 32 || !ensure_targets(width, height)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AoUniforms uniforms{};
|
||||
std::memcpy(uniforms.projection, camera.proj_from_view, sizeof(uniforms.projection));
|
||||
std::memcpy(
|
||||
uniforms.inverse_projection, camera.view_from_proj, sizeof(uniforms.inverse_projection));
|
||||
uniforms.size[0] = static_cast<float>(width);
|
||||
uniforms.size[1] = static_cast<float>(height);
|
||||
uniforms.inv_size[0] = 1.0f / uniforms.size[0];
|
||||
uniforms.inv_size[1] = 1.0f / uniforms.size[1];
|
||||
uniforms.depth_scale[0] = static_cast<float>(resolved.width) / uniforms.size[0];
|
||||
uniforms.depth_scale[1] = static_cast<float>(resolved.height) / uniforms.size[1];
|
||||
uniforms.effect_radius =
|
||||
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarRadius, 70), 10, 500));
|
||||
uniforms.intensity =
|
||||
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarIntensity, 100), 0, 100)) /
|
||||
100.0f;
|
||||
quality_counts(
|
||||
get_int_option(g_cvarQuality, 2), uniforms.slice_count, uniforms.samples_per_slice_side);
|
||||
const uint32_t debugMode =
|
||||
static_cast<uint32_t>(std::clamp<int64_t>(get_int_option(g_cvarDebugView, 0), 0, 4));
|
||||
uniforms.debug_view = debugMode;
|
||||
|
||||
GfxRange uniformRange{0, 0};
|
||||
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &uniformRange) != MOD_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
ComputePayload computePayload{};
|
||||
computePayload.depth = resolved.depth;
|
||||
for (int mip = 0; mip < 5; ++mip) {
|
||||
computePayload.preprocessedDepthMips[mip] = g_targets.preprocessedDepthMips[mip];
|
||||
}
|
||||
computePayload.preprocessedDepthAll = g_targets.preprocessedDepthAll;
|
||||
computePayload.aoNoisy = g_targets.aoNoisyView;
|
||||
computePayload.depthDifferences = g_targets.depthDifferencesView;
|
||||
computePayload.aoFinal = g_targets.aoFinalView;
|
||||
computePayload.uniform_offset = uniformRange.offset;
|
||||
computePayload.uniform_size = uniformRange.size;
|
||||
computePayload.width = width;
|
||||
computePayload.height = height;
|
||||
if (svc_gfx->push_compute(mod_ctx, g_computeType, &computePayload, sizeof(computePayload)) !=
|
||||
MOD_OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const CompositePayload drawPayload{g_targets.aoFinalView, g_targets.preprocessedDepthAll,
|
||||
resolved.depth, uniformRange.offset, uniformRange.size, debugMode};
|
||||
svc_gfx->push_draw(mod_ctx, g_drawType, &drawPayload, sizeof(drawPayload));
|
||||
}
|
||||
|
||||
void add_control(UiElementHandle pane, const UiControlDesc& desc) {
|
||||
svc_ui->pane_add_control(mod_ctx, pane, &desc, nullptr);
|
||||
}
|
||||
|
||||
void add_toggle(UiElementHandle pane, const char* label, ConfigVarHandle cvar, const char* help) {
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_TOGGLE;
|
||||
control.label = label;
|
||||
control.help_rml = help;
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = cvar;
|
||||
add_control(pane, control);
|
||||
}
|
||||
|
||||
ModResult build_controls_tab(
|
||||
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
|
||||
(void)right;
|
||||
|
||||
svc_ui->pane_add_section(mod_ctx, left, "Ambient Occlusion");
|
||||
add_toggle(left, "Enabled", g_cvarEnabled, "Enables the GTAO pass.");
|
||||
|
||||
static const char* kQualityOptions[] = {"Low", "Medium", "High", "Ultra"};
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_SELECT;
|
||||
control.label = "Quality";
|
||||
control.help_rml = "Horizon slices and samples per pixel (XeGTAO presets: 4/8/18/54 spp).";
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = g_cvarQuality;
|
||||
control.options = kQualityOptions;
|
||||
control.option_count = 4;
|
||||
add_control(left, control);
|
||||
|
||||
control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_NUMBER;
|
||||
control.label = "Radius";
|
||||
control.help_rml = "Occlusion sampling radius in world units.";
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = g_cvarRadius;
|
||||
control.min = 10;
|
||||
control.max = 500;
|
||||
control.step = 10;
|
||||
add_control(left, control);
|
||||
|
||||
control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_NUMBER;
|
||||
control.label = "Intensity";
|
||||
control.help_rml = "How strongly occlusion darkens the scene.";
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = g_cvarIntensity;
|
||||
control.min = 0;
|
||||
control.max = 100;
|
||||
control.step = 5;
|
||||
control.suffix = "%";
|
||||
add_control(left, control);
|
||||
|
||||
add_toggle(left, "Half Resolution", g_cvarHalfRes,
|
||||
"Computes AO at half resolution and upscales; faster, slightly softer.");
|
||||
|
||||
static const char* kDebugOptions[] = {"Off", "AO", "Normals", "Depth", "Staircase"};
|
||||
control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_SELECT;
|
||||
control.label = "Debug View";
|
||||
control.help_rml = "AO: raw visibility as grayscale.<br/>Normals: the view-space "
|
||||
"normals the GTAO pass consumes.<br/>Depth: the preprocessed depth "
|
||||
"as a distance gradient.<br/>Staircase: detects quantized depth - smooth "
|
||||
"depth is near-black with thin triangle edges, quantized depth lights "
|
||||
"up across surfaces.";
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = g_cvarDebugView;
|
||||
control.options = kDebugOptions;
|
||||
control.option_count = 5;
|
||||
add_control(left, control);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void on_controls_window_closed(ModContext*, UiWindowHandle, void*) {
|
||||
g_controlsWindow = 0;
|
||||
}
|
||||
|
||||
void on_open_controls(ModContext*, void*) {
|
||||
if (g_controlsWindow != 0) {
|
||||
return;
|
||||
}
|
||||
UiTabDesc tabs[1] = {UI_TAB_DESC_INIT};
|
||||
tabs[0].title = "Controls";
|
||||
tabs[0].build = build_controls_tab;
|
||||
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
|
||||
desc.tabs = tabs;
|
||||
desc.tab_count = 1;
|
||||
desc.on_closed = on_controls_window_closed;
|
||||
if (svc_ui->window_push(mod_ctx, &desc, &g_controlsWindow) != MOD_OK) {
|
||||
svc_log->error(mod_ctx, "failed to open AO controls window");
|
||||
}
|
||||
}
|
||||
|
||||
ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_TOGGLE;
|
||||
control.label = "Enabled";
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = g_cvarEnabled;
|
||||
add_control(panel, control);
|
||||
|
||||
control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_BUTTON;
|
||||
control.label = "Open Controls";
|
||||
control.on_pressed = on_open_controls;
|
||||
add_control(panel, control);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult register_bool_option(
|
||||
const char* name, bool defaultValue, ConfigVarHandle& outHandle, ModError* error) {
|
||||
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
|
||||
cvarDesc.name = name;
|
||||
cvarDesc.type = CONFIG_VAR_BOOL;
|
||||
cvarDesc.default_bool = defaultValue;
|
||||
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to register AO option");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult register_int_option(
|
||||
const char* name, int64_t defaultValue, ConfigVarHandle& outHandle, ModError* error) {
|
||||
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
|
||||
cvarDesc.name = name;
|
||||
cvarDesc.type = CONFIG_VAR_INT;
|
||||
cvarDesc.default_int = defaultValue;
|
||||
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to register AO option");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
ModResult result = svc_resource->load(mod_ctx, "preprocess_depth.wgsl", &g_preprocessSource);
|
||||
if (result == MOD_OK) {
|
||||
result = svc_resource->load(mod_ctx, "gtao.wgsl", &g_gtaoSource);
|
||||
}
|
||||
if (result == MOD_OK) {
|
||||
result = svc_resource->load(mod_ctx, "denoise.wgsl", &g_denoiseSource);
|
||||
}
|
||||
if (result == MOD_OK) {
|
||||
result = svc_resource->load(mod_ctx, "composite.wgsl", &g_compositeSource);
|
||||
}
|
||||
if (result != MOD_OK) {
|
||||
return dusk::mods::set_error(error, result, "failed to load AO shaders");
|
||||
}
|
||||
|
||||
result = register_bool_option("effectEnabled", false, g_cvarEnabled, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
result = register_int_option("quality", 2, g_cvarQuality, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
result = register_int_option("radius", 70, g_cvarRadius, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
result = register_int_option("intensity", 100, g_cvarIntensity, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
result = register_bool_option("halfRes", false, g_cvarHalfRes, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
result = register_int_option("debugMode", 0, g_cvarDebugView, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (svc_gfx->get_device_info(mod_ctx, &g_deviceInfo) != MOD_OK) {
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to query device info");
|
||||
}
|
||||
if (!build_compute_pipeline("AO preprocess depth", g_preprocessSource, "preprocess_depth",
|
||||
g_preprocessPipeline, g_preprocessLayout) ||
|
||||
!build_compute_pipeline("AO downsample mip4", g_preprocessSource, "downsample_mip4",
|
||||
g_mip4Pipeline, g_mip4Layout) ||
|
||||
!build_compute_pipeline("AO gtao", g_gtaoSource, "gtao", g_gtaoPipeline, g_gtaoLayout) ||
|
||||
!build_compute_pipeline(
|
||||
"AO denoise", g_denoiseSource, "spatial_denoise", g_denoisePipeline, g_denoiseLayout))
|
||||
{
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO compute pipelines");
|
||||
}
|
||||
if (!build_composite_pipeline(true, g_compositePipeline, g_compositeLayout) ||
|
||||
!build_composite_pipeline(false, g_compositeDebugPipeline, g_compositeDebugLayout))
|
||||
{
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO composite pipeline");
|
||||
}
|
||||
if (!build_hilbert_lut()) {
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO noise LUT");
|
||||
}
|
||||
|
||||
GfxComputeTypeDesc computeDesc = GFX_COMPUTE_TYPE_DESC_INIT;
|
||||
computeDesc.label = "AO chain";
|
||||
computeDesc.callback = on_compute;
|
||||
if (svc_gfx->register_compute_type(mod_ctx, &computeDesc, &g_computeType) != MOD_OK) {
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to register compute type");
|
||||
}
|
||||
GfxDrawTypeDesc drawDesc = GFX_DRAW_TYPE_DESC_INIT;
|
||||
drawDesc.label = "AO composite";
|
||||
drawDesc.draw = on_draw;
|
||||
if (svc_gfx->register_draw_type(mod_ctx, &drawDesc, &g_drawType) != MOD_OK) {
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to register draw type");
|
||||
}
|
||||
GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT;
|
||||
stageDesc.callback = on_scene_after_opaque;
|
||||
if (svc_gfx->register_stage_hook(
|
||||
mod_ctx, GFX_STAGE_SCENE_AFTER_OPAQUE, &stageDesc, &g_afterOpaqueHook) != MOD_OK)
|
||||
{
|
||||
return dusk::mods::set_error(error, MOD_ERROR, "failed to register stage hook");
|
||||
}
|
||||
|
||||
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
|
||||
panelDesc.build = build_panel;
|
||||
svc_ui->register_mods_panel(mod_ctx, &panelDesc);
|
||||
|
||||
svc_log->info(mod_ctx, "ao_mod ready");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
if (!g_loggedChain && g_chainExecuted.load(std::memory_order_acquire)) {
|
||||
g_loggedChain = true;
|
||||
svc_log->info(mod_ctx, "AO chain executed OK");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
svc_resource->free(mod_ctx, &g_preprocessSource);
|
||||
svc_resource->free(mod_ctx, &g_gtaoSource);
|
||||
svc_resource->free(mod_ctx, &g_denoiseSource);
|
||||
svc_resource->free(mod_ctx, &g_compositeSource);
|
||||
|
||||
release_targets(g_targets);
|
||||
for (auto& retired : g_retiredTargets) {
|
||||
release_targets(retired.targets);
|
||||
}
|
||||
g_retiredTargets.clear();
|
||||
|
||||
const auto releasePipeline = [](WGPUComputePipeline& pipeline) {
|
||||
if (pipeline != nullptr) {
|
||||
wgpuComputePipelineRelease(pipeline);
|
||||
pipeline = nullptr;
|
||||
}
|
||||
};
|
||||
const auto releaseLayout = [](WGPUBindGroupLayout& layout) {
|
||||
if (layout != nullptr) {
|
||||
wgpuBindGroupLayoutRelease(layout);
|
||||
layout = nullptr;
|
||||
}
|
||||
};
|
||||
releasePipeline(g_preprocessPipeline);
|
||||
releasePipeline(g_mip4Pipeline);
|
||||
releasePipeline(g_gtaoPipeline);
|
||||
releasePipeline(g_denoisePipeline);
|
||||
releaseLayout(g_preprocessLayout);
|
||||
releaseLayout(g_mip4Layout);
|
||||
releaseLayout(g_gtaoLayout);
|
||||
releaseLayout(g_denoiseLayout);
|
||||
if (g_compositePipeline != nullptr) {
|
||||
wgpuRenderPipelineRelease(g_compositePipeline);
|
||||
g_compositePipeline = nullptr;
|
||||
}
|
||||
if (g_compositeDebugPipeline != nullptr) {
|
||||
wgpuRenderPipelineRelease(g_compositeDebugPipeline);
|
||||
g_compositeDebugPipeline = nullptr;
|
||||
}
|
||||
releaseLayout(g_compositeLayout);
|
||||
releaseLayout(g_compositeDebugLayout);
|
||||
if (g_hilbertLutView != nullptr) {
|
||||
wgpuTextureViewRelease(g_hilbertLutView);
|
||||
g_hilbertLutView = nullptr;
|
||||
}
|
||||
if (g_hilbertLut != nullptr) {
|
||||
wgpuTextureRelease(g_hilbertLut);
|
||||
g_hilbertLut = nullptr;
|
||||
}
|
||||
g_cvarEnabled = g_cvarQuality = g_cvarRadius = g_cvarIntensity = 0;
|
||||
g_cvarHalfRes = g_cvarDebugView = 0;
|
||||
g_computeType = g_drawType = 0;
|
||||
g_afterOpaqueHook = 0;
|
||||
g_controlsWindow = 0;
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(shadow_mod CXX)
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
|
||||
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if (DUSK_MOD_USE_FULL_TREE)
|
||||
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
|
||||
else ()
|
||||
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_mod(shadow_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
BUNDLE
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.shadow_mod",
|
||||
"name": "[Demo] Dynamic Shadows",
|
||||
"version": "1.0.0",
|
||||
"author": "encounter",
|
||||
"description": "Demo showcasing dynamic shadow maps: re-renders geometry from the sun (or moon) point of view and composites real-time shadows over the world, with screen-space contact shadows for fine detail."
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// Deferred shadow composite: reconstructs the world position of every scene pixel from the
|
||||
// depth snapshot (CameraService matrices), transforms it into the light's clip space, and
|
||||
// PCF-compares against the shadow map rendered earlier this frame. Drawn as a fullscreen
|
||||
// triangle with multiply blending (srcFactor = Dst, dstFactor = Zero) before the HUD.
|
||||
//
|
||||
// Depth conventions (both reversed-Z): the scene snapshot has 1.0 at the camera near plane;
|
||||
// the shadow map, rendered through the game's GX pipeline with a GC-convention light matrix,
|
||||
// stores clip.z, i.e. 1.0 nearest to the light and 0.0 at the light frustum far plane.
|
||||
//
|
||||
// The optional contact-shadow raymarch follows Panos Karabelas' screen-space shadows
|
||||
// (https://panoskarabelas.com/blog/posts/screen_space_shadows/, MIT via Spartan Engine):
|
||||
// march from the pixel toward the light in view space and mark occlusion when the ray dips
|
||||
// behind the depth buffer within a thickness threshold.
|
||||
|
||||
struct Uniforms {
|
||||
world_from_proj: mat4x4f, // scene depth unproject (camera)
|
||||
view_from_proj: mat4x4f, // scene depth -> view space (contact shadows)
|
||||
proj_from_view: mat4x4f, // view -> clip (contact shadows re-projection)
|
||||
light_vp: mat4x4f, // world -> light receiver projection (UV/depth basis)
|
||||
light_dir_view: vec3f, // direction *toward* the light, view space, normalized
|
||||
bias: f32, // shadow-map depth bias (reversed-depth units)
|
||||
size: vec2f, // shadow map size in texels
|
||||
inv_size: vec2f,
|
||||
strength: f32, // final darkening amount, horizon fade baked in
|
||||
pcf_taps: f32, // 0 = single tap, 1 = 3x3, 2 = 5x5
|
||||
contact_enabled: f32,
|
||||
contact_thickness: f32, // view-space thickness threshold
|
||||
contact_length: f32, // view-space march distance
|
||||
debug_mode: u32, // 0 = composite; nonzero modes are diagnostic views
|
||||
_pad0: f32,
|
||||
_pad1: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var scene_depth: texture_2d<f32>;
|
||||
@group(0) @binding(1) var shadow_map: texture_2d<f32>;
|
||||
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
||||
@group(0) @binding(3) var light_color: texture_2d<f32>;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4f,
|
||||
@location(0) uv: vec2f,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let uv = vec2f(f32((index << 1u) & 2u), f32(index & 2u));
|
||||
out.position = vec4f(uv * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), 0.0, 1.0);
|
||||
out.uv = uv;
|
||||
return out;
|
||||
}
|
||||
|
||||
fn load_shadow(texel: vec2<i32>) -> f32 {
|
||||
let clamped = clamp(texel, vec2<i32>(0i), vec2<i32>(uniforms.size) - 1i);
|
||||
return textureLoad(shadow_map, clamped, 0i).r;
|
||||
}
|
||||
|
||||
// Returns 1.0 when the pixel at light-space depth `receiver` is shadowed by the map texel.
|
||||
fn shadow_test(texel: vec2<i32>, receiver: f32) -> f32 {
|
||||
// Reversed depth: a larger stored value is closer to the light, i.e. an occluder.
|
||||
return select(0.0, 1.0, load_shadow(texel) > receiver + uniforms.bias);
|
||||
}
|
||||
|
||||
// Bilinearly weighted comparison (what a hardware comparison sampler would do): filter the
|
||||
// four *comparison results*, never the depths themselves. This is what turns per-texel
|
||||
// staircases into smooth penumbra edges.
|
||||
fn shadow_compare_bilinear(light_uv: vec2f, receiver: f32) -> f32 {
|
||||
let coordinates = light_uv * uniforms.size - 0.5;
|
||||
let base = floor(coordinates);
|
||||
let fraction = coordinates - base;
|
||||
let texel = vec2<i32>(base);
|
||||
let s00 = shadow_test(texel, receiver);
|
||||
let s10 = shadow_test(texel + vec2<i32>(1i, 0i), receiver);
|
||||
let s01 = shadow_test(texel + vec2<i32>(0i, 1i), receiver);
|
||||
let s11 = shadow_test(texel + vec2<i32>(1i, 1i), receiver);
|
||||
let top = mix(s00, s10, fraction.x);
|
||||
let bottom = mix(s01, s11, fraction.x);
|
||||
return mix(top, bottom, fraction.y);
|
||||
}
|
||||
|
||||
fn sample_shadow_pcf(light_uv: vec2f, receiver: f32) -> f32 {
|
||||
let radius = i32(uniforms.pcf_taps);
|
||||
var sum = 0.0;
|
||||
var count = 0.0;
|
||||
for (var y = -radius; y <= radius; y += 1i) {
|
||||
for (var x = -radius; x <= radius; x += 1i) {
|
||||
let offset = vec2f(f32(x), f32(y)) * uniforms.inv_size;
|
||||
sum += shadow_compare_bilinear(light_uv + offset, receiver);
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
return sum / count;
|
||||
}
|
||||
|
||||
fn scene_depth_at(uv: vec2f) -> f32 {
|
||||
let size = vec2<i32>(textureDimensions(scene_depth));
|
||||
let texel = clamp(vec2<i32>(uv * vec2f(size)), vec2<i32>(0i), size - 1i);
|
||||
return textureLoad(scene_depth, texel, 0i).r;
|
||||
}
|
||||
|
||||
fn light_color_at(uv: vec2f) -> vec4f {
|
||||
let size = vec2<i32>(textureDimensions(light_color));
|
||||
let texel = clamp(vec2<i32>(uv * vec2f(size)), vec2<i32>(0i), size - 1i);
|
||||
return textureLoad(light_color, texel, 0i);
|
||||
}
|
||||
|
||||
fn light_depth_debug_at(uv: vec2f) -> vec3f {
|
||||
let texel = vec2<i32>(uv * uniforms.size);
|
||||
let depth = load_shadow(texel);
|
||||
if depth <= 0.0 {
|
||||
return vec3f(0.0);
|
||||
}
|
||||
|
||||
let dx = abs(depth - load_shadow(texel + vec2<i32>(1i, 0i)));
|
||||
let dy = abs(depth - load_shadow(texel + vec2<i32>(0i, 1i)));
|
||||
let edge = saturate((dx + dy) * 500.0);
|
||||
let shade = saturate(depth * 1.5);
|
||||
let bands = 0.08 * (0.5 + 0.5 * cos(depth * 96.0));
|
||||
return vec3f(saturate(shade + bands + edge));
|
||||
}
|
||||
|
||||
fn view_position(uv: vec2f, depth: f32) -> vec3f {
|
||||
let ndc = vec4f(uv.x * 2.0 - 1.0, 1.0 - 2.0 * uv.y, depth, 1.0);
|
||||
let position = uniforms.view_from_proj * ndc;
|
||||
return position.xyz / position.w;
|
||||
}
|
||||
|
||||
// Interleaved gradient noise (Jimenez); fixed per-pixel dither, no temporal rotation.
|
||||
fn ign(pixel: vec2f) -> f32 {
|
||||
return fract(52.9829189 * fract(dot(pixel, vec2f(0.06711056, 0.00583715))));
|
||||
}
|
||||
|
||||
// Screen-space contact shadows: march toward the light in view space; occluded when the ray
|
||||
// passes behind the depth buffer by less than the thickness threshold. Faded out with view
|
||||
// distance: position reconstruction error grows with distance while the thickness threshold
|
||||
// is fixed, so far surfaces (and anything translucent composited over them - clouds, fog)
|
||||
// would otherwise pick up dithered false occlusion. Contact shadows are a near-field effect.
|
||||
fn contact_shadow_fade(view_distance: f32) -> f32 {
|
||||
return saturate(1.0 - (view_distance - 3000.0) / 5000.0);
|
||||
}
|
||||
|
||||
fn contact_shadow(origin: vec3f, pixel: vec2f) -> f32 {
|
||||
let steps = 24;
|
||||
let step_vec = uniforms.light_dir_view * (uniforms.contact_length / f32(steps));
|
||||
var ray = origin + step_vec * ign(pixel);
|
||||
for (var i = 0; i < steps; i += 1) {
|
||||
ray += step_vec;
|
||||
// Project the ray position back to screen space.
|
||||
let clip = uniforms.proj_from_view * vec4f(ray, 1.0);
|
||||
if clip.w <= 0.0 {
|
||||
break;
|
||||
}
|
||||
let ray_ndc = clip.xyz / clip.w;
|
||||
let ray_uv = vec2f(0.5 + 0.5 * ray_ndc.x, 0.5 - 0.5 * ray_ndc.y);
|
||||
if any(ray_uv < vec2f(0.0)) || any(ray_uv > vec2f(1.0)) {
|
||||
break;
|
||||
}
|
||||
let scene = scene_depth_at(ray_uv);
|
||||
if scene <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
// Compare in view space: positive delta = the ray is behind the scene surface.
|
||||
let scene_z = view_position(ray_uv, scene).z;
|
||||
let delta = scene_z - ray.z; // view space looks down -z; larger z = closer
|
||||
if delta > 0.0 && delta < uniforms.contact_thickness {
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
||||
let depth = scene_depth_at(in.uv);
|
||||
if uniforms.debug_mode == 1u {
|
||||
let value = load_shadow(vec2<i32>(in.uv * uniforms.size));
|
||||
return vec4f(value, value, value, 1.0);
|
||||
}
|
||||
if uniforms.debug_mode == 9u || uniforms.debug_mode == 10u {
|
||||
let color = light_color_at(in.uv);
|
||||
let color_luma = max(color.r, max(color.g, color.b));
|
||||
let depth_color = light_depth_debug_at(in.uv);
|
||||
let rgb = select(depth_color, color.rgb, color_luma > (1.0 / 255.0));
|
||||
return vec4f(rgb, 1.0);
|
||||
}
|
||||
|
||||
if depth <= 0.0 {
|
||||
// Sky / cleared pixels receive no shadow.
|
||||
if uniforms.debug_mode >= 3u {
|
||||
return vec4f(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
return vec4f(1.0);
|
||||
}
|
||||
|
||||
let ndc = vec4f(in.uv.x * 2.0 - 1.0, 1.0 - 2.0 * in.uv.y, depth, 1.0);
|
||||
let world4 = uniforms.world_from_proj * ndc;
|
||||
let world = world4.xyz / world4.w;
|
||||
|
||||
let light_clip = uniforms.light_vp * vec4f(world, 1.0);
|
||||
let light_ndc = light_clip.xyz / light_clip.w;
|
||||
let receiver = light_ndc.z; // reversed light depth, 1 = nearest to the light
|
||||
let light_uv = vec2f(0.5 + 0.5 * light_ndc.x, 0.5 - 0.5 * light_ndc.y);
|
||||
let in_shadow_bounds = all(light_uv >= vec2f(0.0)) && all(light_uv <= vec2f(1.0)) &&
|
||||
receiver > 0.0 && receiver <= 1.0;
|
||||
let shadow_depth = load_shadow(vec2<i32>(light_uv * uniforms.size));
|
||||
|
||||
if uniforms.debug_mode == 4u {
|
||||
let valid = select(0.0, 1.0, in_shadow_bounds);
|
||||
return vec4f(saturate(light_uv.x), saturate(light_uv.y), valid, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 5u {
|
||||
if !in_shadow_bounds {
|
||||
return vec4f(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
let current_compare = select(0.0, 1.0, shadow_depth > receiver + uniforms.bias);
|
||||
let opposite_compare = select(0.0, 1.0, shadow_depth < receiver - uniforms.bias);
|
||||
return vec4f(current_compare, 0.0, opposite_compare, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 6u {
|
||||
let valid = select(0.0, 1.0, in_shadow_bounds);
|
||||
return vec4f(saturate(receiver), shadow_depth, valid, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 7u {
|
||||
let beyond_far = select(0.0, 1.0, receiver <= 0.0);
|
||||
let valid_depth = select(0.0, 1.0, receiver > 0.0 && receiver <= 1.0);
|
||||
let before_near = select(0.0, 1.0, receiver > 1.0);
|
||||
return vec4f(beyond_far, valid_depth, before_near, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 8u {
|
||||
let valid_x = select(0.0, 1.0, light_uv.x >= 0.0 && light_uv.x <= 1.0);
|
||||
let valid_y = select(0.0, 1.0, light_uv.y >= 0.0 && light_uv.y <= 1.0);
|
||||
let valid_depth = select(0.0, 1.0, receiver > 0.0 && receiver <= 1.0);
|
||||
return vec4f(valid_x, valid_y, valid_depth, 1.0);
|
||||
}
|
||||
|
||||
var occlusion = 0.0;
|
||||
if in_shadow_bounds {
|
||||
occlusion = sample_shadow_pcf(light_uv, receiver);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 3u {
|
||||
return vec4f(occlusion, occlusion, occlusion, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.contact_enabled != 0.0 && occlusion < 1.0 {
|
||||
let origin = view_position(in.uv, depth);
|
||||
let fade = contact_shadow_fade(-origin.z);
|
||||
if fade > 0.0 {
|
||||
occlusion = max(occlusion, fade * contact_shadow(origin, in.position.xy));
|
||||
}
|
||||
}
|
||||
|
||||
let value = 1.0 - uniforms.strength * occlusion;
|
||||
if uniforms.debug_mode == 2u {
|
||||
return vec4f(value, value, value, 1.0);
|
||||
}
|
||||
return vec4f(value, value, value, 1.0);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(template_mod CXX)
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
|
||||
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if (DUSK_MOD_USE_FULL_TREE)
|
||||
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
|
||||
else ()
|
||||
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_mod(template_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "com.example.mod",
|
||||
"name": "Template Mod",
|
||||
"version": "1.0.0",
|
||||
"author": "You",
|
||||
"description": "An example Dusklight mod"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/log.h"
|
||||
|
||||
DEFINE_MOD();
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
|
||||
extern "C" {
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError*) {
|
||||
svc_log->info(mod_ctx, "template_mod initialized");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
svc_log->info(mod_ctx, "template_mod unloaded");
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
@@ -14,12 +15,16 @@ import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
import org.libsdl.app.SDLSurface;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
@@ -27,6 +32,7 @@ import java.util.List;
|
||||
|
||||
public class DuskActivity extends SDLActivity {
|
||||
private static final String TAG = "DuskActivity";
|
||||
private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f;
|
||||
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
|
||||
private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456;
|
||||
private static final String EXTERNAL_STORAGE_AUTHORITY =
|
||||
@@ -88,6 +94,11 @@ public class DuskActivity extends SDLActivity {
|
||||
hideSystemBars();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SDLSurface createSDLSurface(Context context) {
|
||||
return new DuskSurface(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
@@ -139,6 +150,77 @@ public class DuskActivity extends SDLActivity {
|
||||
};
|
||||
}
|
||||
|
||||
public void setPreferredSurfaceFrameRate(float frameRate) {
|
||||
runOnUiThread(() -> {
|
||||
if (mSurface instanceof DuskSurface) {
|
||||
((DuskSurface)mSurface).setPreferredFrameRate(frameRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final class DuskSurface extends SDLSurface {
|
||||
private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE;
|
||||
|
||||
DuskSurface(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
super.surfaceChanged(holder, format, width, height);
|
||||
setTargetFrameRate(holder);
|
||||
}
|
||||
|
||||
void setPreferredFrameRate(float frameRate) {
|
||||
preferredFrameRate = frameRate;
|
||||
setTargetFrameRate(getHolder());
|
||||
}
|
||||
|
||||
private void setTargetFrameRate(SurfaceHolder holder) {
|
||||
if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return;
|
||||
}
|
||||
|
||||
Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface();
|
||||
if (surface == null || !surface.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float targetFrameRate = getMaxSupportedFrameRate();
|
||||
if (preferredFrameRate > 0.0f) {
|
||||
targetFrameRate = preferredFrameRate;
|
||||
}
|
||||
if (targetFrameRate <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
surface.setFrameRate(
|
||||
targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
|
||||
Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps");
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Failed to request surface frame rate", e);
|
||||
}
|
||||
}
|
||||
|
||||
private float getMaxSupportedFrameRate() {
|
||||
if (mDisplay == null) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float maxFrameRate = mDisplay.getRefreshRate();
|
||||
Display.Mode[] modes = mDisplay.getSupportedModes();
|
||||
if (modes == null) {
|
||||
return maxFrameRate;
|
||||
}
|
||||
|
||||
for (Display.Mode mode : modes) {
|
||||
maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate());
|
||||
}
|
||||
return maxFrameRate;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getArguments() {
|
||||
Intent intent = getIntent();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,97 @@
|
||||
window.logs content {
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
window.logs .log-toolbar {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex: 0 0 64dp;
|
||||
height: 64dp;
|
||||
align-items: center;
|
||||
gap: 8dp;
|
||||
padding-right: 72dp;
|
||||
background-color: rgba(217, 217, 217, 10%);
|
||||
border-bottom: 2dp #92875B;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 18dp;
|
||||
}
|
||||
|
||||
window.logs > close {
|
||||
top: 8dp;
|
||||
right: 8dp;
|
||||
}
|
||||
|
||||
window.logs .log-title {
|
||||
align-self: stretch;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 24dp;
|
||||
line-height: 64dp;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 4dp #C2A42D;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
}
|
||||
|
||||
window.logs .log-title-mod {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-family: "Fira Sans";
|
||||
font-weight: normal;
|
||||
font-size: 15dp;
|
||||
color: rgba(224, 219, 200, 55%);
|
||||
}
|
||||
|
||||
.log-toolbar-spacer {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.log-toolbar button {
|
||||
flex: 0 0 auto;
|
||||
font-family: "Fira Sans";
|
||||
font-weight: normal;
|
||||
font-size: 15dp;
|
||||
padding: 5dp 12dp;
|
||||
}
|
||||
|
||||
window.logs content pane.log-view {
|
||||
flex: 1 1 0;
|
||||
padding: 12dp 16dp;
|
||||
padding-bottom: 0dp;
|
||||
gap: 0dp;
|
||||
}
|
||||
|
||||
.log-lines {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
display: block;
|
||||
font-family: "Noto Mono";
|
||||
font-size: 13dp;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.log-line .log-time {
|
||||
color: rgba(224, 219, 200, 45%);
|
||||
}
|
||||
|
||||
.log-line .log-mod {
|
||||
color: rgba(194, 164, 45, 80%);
|
||||
}
|
||||
|
||||
.log-line.lvl-trace,
|
||||
.log-line.lvl-debug {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.log-line.lvl-warn .log-msg {
|
||||
color: #ffa826;
|
||||
}
|
||||
|
||||
.log-line.lvl-error .log-msg {
|
||||
color: #cc4444;
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
window.mods content pane.mod-list {
|
||||
flex: 0 0 360dp;
|
||||
padding: 16dp;
|
||||
padding-bottom: 0dp;
|
||||
gap: 4dp;
|
||||
}
|
||||
|
||||
@media (max-height: 640dp) {
|
||||
window.mods content pane.mod-list {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
window.mods content pane.mod-detail {
|
||||
gap: 12dp;
|
||||
}
|
||||
|
||||
.mod-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12dp;
|
||||
padding: 4dp 0;
|
||||
}
|
||||
|
||||
.mod-info-label {
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
opacity: 0.55;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mod-info-value {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.mod-path {
|
||||
font-size: 14dp;
|
||||
word-break: break-all;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
mod-entry {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 12dp;
|
||||
padding: 10dp;
|
||||
border-radius: 10dp;
|
||||
decorator: vertical-gradient(#c2a42d00 #c2a42d00);
|
||||
transition: decorator 0.1s linear-in-out;
|
||||
cursor: pointer;
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
mod-entry.current {
|
||||
box-shadow: rgba(146, 135, 91, 40%) 0 0 0 1dp;
|
||||
}
|
||||
|
||||
mod-entry:hover,
|
||||
mod-entry:focus-visible {
|
||||
decorator: vertical-gradient(#c2a42d00 #c2a42d26);
|
||||
}
|
||||
|
||||
mod-entry:selected {
|
||||
decorator: vertical-gradient(#c2a42d10 #c2a42d40);
|
||||
}
|
||||
|
||||
mod-entry .mod-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 56dp;
|
||||
height: 56dp;
|
||||
border-radius: 8dp;
|
||||
}
|
||||
|
||||
mod-entry icon.mod-icon {
|
||||
font-size: 36dp;
|
||||
background-color: rgba(17, 16, 10, 20%);
|
||||
color: rgba(224, 219, 200, 45%);
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-info {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
gap: 2dp;
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-name {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: baseline;
|
||||
gap: 6dp;
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-name-text {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-version {
|
||||
flex: 0 0 auto;
|
||||
font-size: 13dp;
|
||||
color: rgba(224, 219, 200, 50%);
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-status.active {
|
||||
color: #44cc55;
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-status.failed {
|
||||
color: #cc4444;
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-desc {
|
||||
font-size: 14dp;
|
||||
line-height: 1.3;
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
max-height: 2.6em;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
mod-entry .mod-entry-sub {
|
||||
font-size: 13dp;
|
||||
color: rgba(224, 219, 200, 50%);
|
||||
}
|
||||
|
||||
mod-entry.inactive .mod-icon {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
mod-entry.inactive .mod-entry-info {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
mod-header {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
mod-header.has-banner {
|
||||
height: 180dp;
|
||||
margin: -24dp -24dp 0dp -24dp;
|
||||
}
|
||||
|
||||
mod-header .mod-actions {
|
||||
position: absolute;
|
||||
top: 24dp;
|
||||
left: 24dp;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 8dp;
|
||||
}
|
||||
|
||||
mod-header .mod-actions button {
|
||||
font-size: 16dp;
|
||||
padding: 6dp 14dp;
|
||||
background-color: rgba(21, 22, 16, 80%);
|
||||
box-shadow: rgba(146, 135, 91, 60%) 0 0 0 1dp;
|
||||
}
|
||||
|
||||
mod-header.no-banner {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mod-header.no-banner .mod-actions {
|
||||
position: static;
|
||||
}
|
||||
|
||||
window.mods .mod-title {
|
||||
display: block;
|
||||
font-size: 28dp;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
window.mods .mod-title .mod-title-version {
|
||||
font-weight: normal;
|
||||
font-size: 16dp;
|
||||
color: rgba(224, 219, 200, 55%);
|
||||
}
|
||||
|
||||
window.mods .mod-author {
|
||||
display: block;
|
||||
font-size: 15dp;
|
||||
color: rgba(224, 219, 200, 55%);
|
||||
}
|
||||
|
||||
window.mods .mod-restart-note {
|
||||
font-size: 15dp;
|
||||
color: #ffa826;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
window.mods .mod-description {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 14dp;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.status-badge.active,
|
||||
.mod-info-label.active {
|
||||
color: #44cc55;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status-badge.failed,
|
||||
.mod-info-label.failed {
|
||||
color: #cc4444;
|
||||
opacity: 1;
|
||||
}
|
||||
+60
-2
@@ -21,6 +21,7 @@ body {
|
||||
}
|
||||
|
||||
fps,
|
||||
pipeline-progress,
|
||||
toast {
|
||||
position: absolute;
|
||||
border: 1dp #92875B;
|
||||
@@ -98,7 +99,7 @@ toast message row.muted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
toast progress {
|
||||
progress {
|
||||
height: 4dp;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -106,10 +107,50 @@ toast progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
toast progress fill {
|
||||
progress fill {
|
||||
background-color: rgba(194, 164, 45, 80%);
|
||||
}
|
||||
|
||||
pipeline-progress {
|
||||
left: 12dp;
|
||||
bottom: 12dp;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
z-index: 100;
|
||||
min-width: 260dp;
|
||||
max-width: 90%;
|
||||
padding: 10dp 16dp 12dp;
|
||||
border-radius: 7dp;
|
||||
overflow: hidden;
|
||||
filter: opacity(0);
|
||||
transition: filter 0.2s linear-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
pipeline-progress[open] {
|
||||
filter: opacity(1);
|
||||
}
|
||||
|
||||
pipeline-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8dp;
|
||||
font-size: 18dp;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
icon.pipeline-spinner {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
line-height: 1.2em;
|
||||
font-size: 1.2em;
|
||||
color: #C2A42D;
|
||||
text-align: center;
|
||||
transform-origin: center;
|
||||
animation: 1s linear infinite pipeline-spinner-spin;
|
||||
}
|
||||
|
||||
toast.achievement {
|
||||
border: 1dp #C2A42D;
|
||||
}
|
||||
@@ -118,6 +159,14 @@ toast.achievement heading {
|
||||
color: #C2A42D;
|
||||
}
|
||||
|
||||
toast.warning {
|
||||
border: 1dp #C2A42D;
|
||||
}
|
||||
|
||||
toast.warning heading {
|
||||
color: #C2A42D;
|
||||
}
|
||||
|
||||
toast.controller-warning {
|
||||
top: auto;
|
||||
right: auto;
|
||||
@@ -310,6 +359,15 @@ logo img.outer {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pipeline-spinner-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 640dp) {
|
||||
toast {
|
||||
top: 20dp;
|
||||
|
||||
@@ -362,6 +362,10 @@ body.animate-in .intro-item {
|
||||
transition: opacity transform 0.3s 0.6s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-6 {
|
||||
transition: opacity transform 0.3s 0.7s cubic-in-out;
|
||||
}
|
||||
|
||||
/* Mobile layout */
|
||||
@media (max-height: 640dp) {
|
||||
.gradient {
|
||||
|
||||
+12
-3
@@ -50,7 +50,8 @@ tab-bar[closable] tab-end-spacer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
tab-bar[closable] close {
|
||||
tab-bar[closable] close,
|
||||
window > close {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 8dp;
|
||||
@@ -70,12 +71,20 @@ tab-bar[closable] close {
|
||||
}
|
||||
|
||||
tab-bar[closable] close:hover,
|
||||
tab-bar[closable] close:focus-visible {
|
||||
tab-bar[closable] close:focus-visible,
|
||||
window > close:hover,
|
||||
window > close:focus-visible {
|
||||
color: #fff;
|
||||
background-color: rgba(194, 164, 45, 24%);
|
||||
}
|
||||
|
||||
tab-bar[closable] close:active {
|
||||
window > close {
|
||||
top: 16dp;
|
||||
right: 16dp;
|
||||
}
|
||||
|
||||
tab-bar[closable] close:active,
|
||||
window > close:active {
|
||||
color: #fff;
|
||||
background-color: rgba(194, 164, 45, 40%);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,6 @@ action-bar {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
border: 1dp rgba(255, 255, 255, 22%);
|
||||
border-radius: 23dp;
|
||||
background-color: rgba(22, 24, 28, 48%);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Dusklight Mod SDK entry point
|
||||
#
|
||||
# Provides game/service headers, compile definitions, version.h and WebGPU
|
||||
# headers without configuring the full game tree.
|
||||
#
|
||||
# Usage (from a mod project):
|
||||
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
|
||||
# add_mod(my_mod SOURCES ... MOD_JSON mod.json)
|
||||
#
|
||||
# On Windows, pass -DDUSK_GAME_IMPLIB=<path to sdk/dusklight.lib> from the
|
||||
# matching game release. TODO: auto-download from tag
|
||||
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
|
||||
if (TARGET dusklight_game_headers)
|
||||
message(FATAL_ERROR "Mod SDK already configured")
|
||||
endif ()
|
||||
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/WindowsTargetProcessor.cmake")
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING
|
||||
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
|
||||
endif ()
|
||||
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Version detection & version.h
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/DetectVersion.cmake")
|
||||
detect_version()
|
||||
configure_version_header()
|
||||
|
||||
# Provides dawn::webgpu_dawn and dawn::dawncpp_headers for public gfx service headers.
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../extern/aurora/cmake/AuroraDependencyVersions.cmake")
|
||||
set(AURORA_DAWN_PROVIDER "package" CACHE STRING "How to provide Dawn for the mod SDK")
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../extern/aurora/cmake/AuroraDawnProvider.cmake")
|
||||
|
||||
# Game ABI headers & compile definitions
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GameABIConfig.cmake")
|
||||
|
||||
if (WIN32)
|
||||
set(DUSK_GAME_IMPLIB "" CACHE FILEPATH "Path to the dusklight import library (sdk/dusklight.lib)")
|
||||
if (DUSK_GAME_IMPLIB AND NOT EXISTS "${DUSK_GAME_IMPLIB}")
|
||||
message(FATAL_ERROR "Mod SDK: DUSK_GAME_IMPLIB does not exist: ${DUSK_GAME_IMPLIB}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/ModSDK.cmake")
|
||||
@@ -0,0 +1,139 @@
|
||||
// Mod SDK runtime (Windows): applies lld's MinGW-style runtime pseudo-relocations on
|
||||
// the plain MSVC CRT. This is what lets mods reference game data (`extern` globals) with no
|
||||
// __declspec(dllimport) annotations: `lld-link -lldmingw` auto-imports the data through IAT
|
||||
// slots and records fixups, and this module applies them at load time.
|
||||
//
|
||||
// The fixup pass runs as an early CRT initializer, and the IAT slots it reads were already
|
||||
// bound by the OS loader, so even mod static initializers observe patched references.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" char __RUNTIME_PSEUDO_RELOC_LIST__;
|
||||
extern "C" char __RUNTIME_PSEUDO_RELOC_LIST_END__;
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
namespace {
|
||||
|
||||
struct HdrV2 {
|
||||
uint32_t magic1;
|
||||
uint32_t magic2;
|
||||
uint32_t version;
|
||||
};
|
||||
struct ItemV2 {
|
||||
uint32_t sym; // RVA of the __imp_ slot (OS-bound IAT entry)
|
||||
uint32_t target; // RVA of the reference to patch
|
||||
uint32_t flags; // low 8 bits: bit width of the reference
|
||||
};
|
||||
|
||||
bool g_relocsFailed = false;
|
||||
|
||||
void report(const char* fmt, ...) {
|
||||
char buf[512];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
fprintf(stderr, "%s\n", buf);
|
||||
OutputDebugStringA(buf);
|
||||
}
|
||||
|
||||
bool compute_fixup(const ItemV2& item, char* base, intptr_t* out) {
|
||||
char* impSlot = base + item.sym;
|
||||
const intptr_t real = *reinterpret_cast<intptr_t*>(impSlot);
|
||||
char* target = base + item.target;
|
||||
const int bits = static_cast<int>(item.flags & 0xff);
|
||||
|
||||
intptr_t reldata;
|
||||
switch (bits) {
|
||||
case 8:
|
||||
reldata = *reinterpret_cast<int8_t*>(target);
|
||||
break;
|
||||
case 16:
|
||||
reldata = *reinterpret_cast<int16_t*>(target);
|
||||
break;
|
||||
case 32:
|
||||
reldata = *reinterpret_cast<int32_t*>(target);
|
||||
break;
|
||||
case 64:
|
||||
reldata = *reinterpret_cast<int64_t*>(target);
|
||||
break;
|
||||
default:
|
||||
report("unsupported %d-bit pseudo-relocation at RVA 0x%x", bits, item.target);
|
||||
return false;
|
||||
}
|
||||
reldata -= reinterpret_cast<intptr_t>(impSlot);
|
||||
reldata += real;
|
||||
|
||||
if (bits < 64) {
|
||||
const intptr_t maxUnsigned = (intptr_t{1} << bits) - 1;
|
||||
const intptr_t minSigned = -(intptr_t{1} << (bits - 1));
|
||||
if (reldata > maxUnsigned || reldata < minSigned) {
|
||||
report("%d-bit data fixup at RVA 0x%x is out of range (delta %+lld)", bits, item.target,
|
||||
static_cast<long long>(reldata));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*out = reldata;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// lld refuses to emit runtime pseudo-relocs unless a function with exactly this (mingw CRT)
|
||||
// name exists in the image. It is also our actual entry point.
|
||||
extern "C" void _pei386_runtime_relocator() {
|
||||
char* base = reinterpret_cast<char*>(&__ImageBase);
|
||||
char* start = &__RUNTIME_PSEUDO_RELOC_LIST__;
|
||||
char* end = &__RUNTIME_PSEUDO_RELOC_LIST_END__;
|
||||
if (end - start < static_cast<ptrdiff_t>(sizeof(HdrV2))) {
|
||||
return; // no data auto-imports in this mod
|
||||
}
|
||||
const HdrV2* hdr = reinterpret_cast<const HdrV2*>(start);
|
||||
if (hdr->magic1 != 0 || hdr->magic2 != 0 || hdr->version != 1) {
|
||||
report("unexpected pseudo-relocation list format");
|
||||
g_relocsFailed = true;
|
||||
return;
|
||||
}
|
||||
const ItemV2* items = reinterpret_cast<const ItemV2*>(hdr + 1);
|
||||
const ItemV2* itemsEnd = reinterpret_cast<const ItemV2*>(end);
|
||||
|
||||
// Validate everything before writing anything, so a bad fixup can't leave the image
|
||||
// half-patched.
|
||||
intptr_t scratch;
|
||||
for (const ItemV2* it = items; it < itemsEnd; ++it) {
|
||||
if (!compute_fixup(*it, base, &scratch)) {
|
||||
g_relocsFailed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const ItemV2* it = items; it < itemsEnd; ++it) {
|
||||
intptr_t reldata;
|
||||
compute_fixup(*it, base, &reldata);
|
||||
char* target = base + it->target;
|
||||
const size_t len = static_cast<size_t>(it->flags & 0xff) / 8;
|
||||
DWORD old = 0;
|
||||
if (!VirtualProtect(target, len, PAGE_EXECUTE_READWRITE, &old)) {
|
||||
report("VirtualProtect failed at RVA 0x%x", it->target);
|
||||
g_relocsFailed = true;
|
||||
return;
|
||||
}
|
||||
std::memcpy(target, &reldata, len);
|
||||
VirtualProtect(target, len, old, &old);
|
||||
}
|
||||
}
|
||||
|
||||
using PVFV = void (*)();
|
||||
#pragma section(".CRT$XCB", read)
|
||||
extern "C" __declspec(allocate(".CRT$XCB")) PVFV dusk_mod_pseudo_reloc_init =
|
||||
_pei386_runtime_relocator;
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID) {
|
||||
if (reason == DLL_PROCESS_ATTACH && g_relocsFailed) {
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "os_report.h"
|
||||
|
||||
Z2SoundObjBase::Z2SoundObjBase()
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
: JSULink<Z2SoundObjBase>(this)
|
||||
#endif
|
||||
{
|
||||
|
||||
@@ -175,3 +175,13 @@ bool daAlink_c::checkAimContext() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool daAlink_c::checkAimInputContext() {
|
||||
switch (mProcID) {
|
||||
case PROC_HOOKSHOT_ROOF_WAIT:
|
||||
case PROC_HOOKSHOT_WALL_WAIT:
|
||||
return false;
|
||||
default:
|
||||
return checkAimContext();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,18 +122,11 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
||||
var_f31 /= dComIfGp_getCameraZoomScale(field_0x317c);
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMouseAim && checkAimContext()) {
|
||||
sp8 = mBodyAngle.x;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
|
||||
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
|
||||
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
|
||||
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
|
||||
|
||||
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
|
||||
sp8 = mBodyAngle.x;
|
||||
}
|
||||
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
|
||||
sp8 = mBodyAngle.x;
|
||||
}
|
||||
} else {
|
||||
sp8 = mBodyAngle.x;
|
||||
@@ -142,7 +135,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
||||
#if TARGET_PC
|
||||
if ((dusk::getSettings().game.enableGyroAim ||
|
||||
dusk::getSettings().game.enableMouseAim) &&
|
||||
checkAimContext())
|
||||
checkAimInputContext())
|
||||
{
|
||||
f32 gyro_scale = 1.0f;
|
||||
if (checkWolfEyeUp()) {
|
||||
@@ -156,7 +149,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
||||
f32 final_yaw = 0.f;
|
||||
f32 final_pitch = 0.f;
|
||||
if (dusk::getSettings().game.enableMouseAim) {
|
||||
dusk::mouse::getAimDeltas(final_yaw, final_pitch);
|
||||
dusk::mouse::get_aim_deltas(final_yaw, final_pitch);
|
||||
}
|
||||
if (dusk::getSettings().game.enableGyroAim) {
|
||||
f32 gyro_yaw = 0.f;
|
||||
@@ -174,7 +167,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
||||
}
|
||||
}
|
||||
|
||||
if (dusk::getSettings().game.enableTouchControls && checkAimContext()) {
|
||||
if (dusk::getSettings().game.enableTouchControls && checkAimInputContext()) {
|
||||
f32 touchYawDp = 0.0f;
|
||||
f32 touchPitchDp = 0.0f;
|
||||
if (dusk::touch_camera::consume_delta(touchYawDp, touchPitchDp)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user