mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-11 05:04:40 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c509ff7647 |
+1
-4
@@ -304,7 +304,6 @@ source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
|
|||||||
include(cmake/GameABIConfig.cmake)
|
include(cmake/GameABIConfig.cmake)
|
||||||
|
|
||||||
find_package(Threads REQUIRED)
|
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
|
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
|
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
|
||||||
Threads::Threads zstd::libzstd dusklight_game_headers)
|
Threads::Threads zstd::libzstd dusklight_game_headers)
|
||||||
@@ -560,9 +559,7 @@ include(cmake/ModSDK.cmake)
|
|||||||
|
|
||||||
if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
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_custom_target(dusklight_mods) # Aggregate target for all in-tree mods
|
||||||
add_subdirectory(mods/template_mod)
|
add_subdirectory(tools/mod_template)
|
||||||
add_subdirectory(mods/ao_mod)
|
|
||||||
add_subdirectory(mods/shadow_mod)
|
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
|
|||||||
@@ -25,8 +25,3 @@ set(_game_include_dirs
|
|||||||
add_library(dusklight_game_headers INTERFACE)
|
add_library(dusklight_game_headers INTERFACE)
|
||||||
target_include_directories(dusklight_game_headers INTERFACE ${_game_include_dirs})
|
target_include_directories(dusklight_game_headers INTERFACE ${_game_include_dirs})
|
||||||
target_compile_definitions(dusklight_game_headers INTERFACE ${_game_compile_defs})
|
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 ()
|
|
||||||
|
|||||||
+71
-87
@@ -54,84 +54,79 @@ function(add_mod target_name)
|
|||||||
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
|
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
set(_has_lib FALSE)
|
add_library(${target_name} SHARED ${ARG_SOURCES})
|
||||||
set(_lib_name "")
|
_mod_lib_name(_lib_name)
|
||||||
if (ARG_SOURCES)
|
set_target_properties(${target_name} PROPERTIES
|
||||||
set(_has_lib TRUE)
|
PREFIX ""
|
||||||
add_library(${target_name} SHARED ${ARG_SOURCES})
|
C_VISIBILITY_PRESET hidden
|
||||||
_mod_lib_name(_lib_name)
|
CXX_VISIBILITY_PRESET hidden
|
||||||
set_target_properties(${target_name} PROPERTIES
|
VISIBILITY_INLINES_HIDDEN ON
|
||||||
PREFIX ""
|
WINDOWS_EXPORT_ALL_SYMBOLS OFF)
|
||||||
C_VISIBILITY_PRESET hidden
|
target_compile_features(${target_name} PRIVATE cxx_std_20)
|
||||||
CXX_VISIBILITY_PRESET hidden
|
target_link_libraries(${target_name} PRIVATE dusklight_game_headers)
|
||||||
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)
|
if (NOT TARGET dusklight)
|
||||||
# Apply global compile options for out-of-tree mod builds
|
# Apply global compile options for out-of-tree mod builds
|
||||||
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||||
target_compile_options(${target_name} PRIVATE
|
target_compile_options(${target_name} PRIVATE
|
||||||
-Wno-multichar -Wno-trigraphs -Wno-deprecated-declarations)
|
-Wno-multichar -Wno-trigraphs -Wno-deprecated-declarations)
|
||||||
elseif (APPLE)
|
elseif (APPLE)
|
||||||
target_compile_options(${target_name} PRIVATE
|
target_compile_options(${target_name} PRIVATE
|
||||||
-Wno-declaration-after-statement -Wno-non-pod-varargs)
|
-Wno-declaration-after-statement -Wno-non-pod-varargs)
|
||||||
elseif (MSVC)
|
elseif (MSVC)
|
||||||
target_compile_options(${target_name} PRIVATE
|
target_compile_options(${target_name} PRIVATE
|
||||||
"$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>"
|
"$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>"
|
||||||
"$<$<COMPILE_LANGUAGE:C,CXX>:/utf-8>")
|
"$<$<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 ()
|
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)
|
if (APPLE)
|
||||||
# Game symbols resolve against the host executable at dlopen time.
|
# Game symbols resolve against the host executable at dlopen time.
|
||||||
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
|
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
|
||||||
elseif (ANDROID)
|
elseif (ANDROID)
|
||||||
if (TARGET dusklight)
|
if (TARGET dusklight)
|
||||||
target_link_libraries(${target_name} PRIVATE dusklight)
|
target_link_libraries(${target_name} PRIVATE dusklight)
|
||||||
elseif (DUSK_GAME_SOLIB)
|
elseif (DUSK_GAME_SOLIB)
|
||||||
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_SOLIB}")
|
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_SOLIB}")
|
||||||
else ()
|
else ()
|
||||||
message(FATAL_ERROR "add_mod: DUSK_GAME_SOLIB is not set (libmain.so)")
|
message(FATAL_ERROR "add_mod: DUSK_GAME_SOLIB is not set (libmain.so)")
|
||||||
endif ()
|
endif ()
|
||||||
elseif (UNIX)
|
elseif (UNIX)
|
||||||
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
|
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
|
||||||
elseif (WIN32)
|
elseif (WIN32)
|
||||||
# Link against the generated import library (game ABI surface). Function calls
|
# Link against the generated import library (game ABI surface). Function calls
|
||||||
# resolve through import thunks. Data is toolchain dependent:
|
# resolve through import thunks. Data is toolchain dependent:
|
||||||
# - clang-cl: lld's mingw mode auto-imports data references, fixed up at load by
|
# - 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).
|
# the mod SDK's pseudo-relocation runtime (pseudo_reloc.cpp).
|
||||||
# - cl (MSVC): only DUSK_GAME_DATA-annotated data is reachable. Un-annotated
|
# - cl (MSVC): only DUSK_GAME_DATA-annotated data is reachable. Un-annotated
|
||||||
# references fail to link.
|
# references fail to link.
|
||||||
if (NOT DUSK_GAME_IMPLIB)
|
if (NOT DUSK_GAME_IMPLIB)
|
||||||
message(FATAL_ERROR "add_mod: DUSK_GAME_IMPLIB is not set.")
|
message(FATAL_ERROR "add_mod: DUSK_GAME_IMPLIB is not set.")
|
||||||
endif ()
|
endif ()
|
||||||
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_IMPLIB}")
|
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_IMPLIB}")
|
||||||
set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||||
target_compile_definitions(${target_name} PRIVATE _ITERATOR_DEBUG_LEVEL=0)
|
target_compile_definitions(${target_name} PRIVATE _ITERATOR_DEBUG_LEVEL=0)
|
||||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
target_compile_options(${target_name} PRIVATE "$<$<COMPILE_LANGUAGE:C,CXX>:/clang:-mcmodel=large>")
|
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")
|
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
|
# lld mingw mode rewrites /DEFAULTLIB directives to -l style and skips %LIB%, so
|
||||||
# the CRT libraries and search paths are spelled out explicitly.
|
# the CRT libraries and search paths are spelled out explicitly.
|
||||||
target_link_options(${target_name} PRIVATE -lldmingw /nodefaultlib /INCREMENTAL:NO)
|
target_link_options(${target_name} PRIVATE -lldmingw /nodefaultlib /INCREMENTAL:NO)
|
||||||
target_link_libraries(${target_name} PRIVATE
|
target_link_libraries(${target_name} PRIVATE
|
||||||
msvcrt.lib msvcprt.lib vcruntime.lib ucrt.lib
|
msvcrt.lib msvcprt.lib vcruntime.lib ucrt.lib
|
||||||
oldnames.lib uuid.lib kernel32.lib user32.lib)
|
oldnames.lib uuid.lib kernel32.lib user32.lib)
|
||||||
set(_lib_dirs "$ENV{LIB}")
|
set(_lib_dirs "$ENV{LIB}")
|
||||||
if ("${_lib_dirs}" STREQUAL "")
|
if ("${_lib_dirs}" STREQUAL "")
|
||||||
message(FATAL_ERROR "add_mod: %LIB% is empty; configure from a VS dev shell")
|
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 ()
|
||||||
|
foreach (_libdir IN LISTS _lib_dirs)
|
||||||
|
target_link_options(${target_name} PRIVATE "/libpath:${_libdir}")
|
||||||
|
endforeach ()
|
||||||
endif ()
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
@@ -146,14 +141,6 @@ function(add_mod target_name)
|
|||||||
set(_package_deps "${_mod_json}")
|
set(_package_deps "${_mod_json}")
|
||||||
set(_package_inputs "${_mod_json}")
|
set(_package_inputs "${_mod_json}")
|
||||||
set(_extra_cmds "")
|
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)
|
if (ARG_RES_DIR)
|
||||||
_mod_resolve_source_path(_res_dir "${ARG_RES_DIR}")
|
_mod_resolve_source_path(_res_dir "${ARG_RES_DIR}")
|
||||||
_mod_collect_assets(_res_deps "${_res_dir}")
|
_mod_collect_assets(_res_deps "${_res_dir}")
|
||||||
@@ -207,12 +194,12 @@ function(add_mod target_name)
|
|||||||
add_custom_command(OUTPUT "${_out}"
|
add_custom_command(OUTPUT "${_out}"
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_stage}"
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_stage}"
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${_output_dir}"
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${_output_dir}"
|
||||||
${_lib_copy_cmd}
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${target_name}>" "${_stage}/${_lib_name}"
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_mod_json}" "${_stage}/mod.json"
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_mod_json}" "${_stage}/mod.json"
|
||||||
${_extra_cmds}
|
${_extra_cmds}
|
||||||
COMMAND ${CMAKE_COMMAND} -E chdir "${_stage}" ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
|
COMMAND ${CMAKE_COMMAND} -E chdir "${_stage}" ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
|
||||||
${_bundle_cmds}
|
${_bundle_cmds}
|
||||||
DEPENDS ${_target_depend} ${_package_deps} "${_package_inputs_file}"
|
DEPENDS ${target_name} ${_package_deps} "${_package_inputs_file}"
|
||||||
COMMENT "Packaging ${target_name} -> ${_out}"
|
COMMENT "Packaging ${target_name} -> ${_out}"
|
||||||
COMMAND_EXPAND_LISTS
|
COMMAND_EXPAND_LISTS
|
||||||
VERBATIM
|
VERBATIM
|
||||||
@@ -268,9 +255,6 @@ function(install_bundled_mods)
|
|||||||
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/Contents/Resources/mods/${_id}")
|
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)")
|
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/Resources/mods/${_id}/${_lib_name}\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||||
endforeach ()
|
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)")
|
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - --entitlements \"${DUSK_ENTITLEMENTS}\" \"${_bundle_dir}\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||||
endif ()
|
endif ()
|
||||||
return ()
|
return ()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ include_guard(GLOBAL)
|
|||||||
|
|
||||||
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||||
|
|
||||||
set(_SYMGEN_VERSION "1.1.1")
|
set(_SYMGEN_VERSION "1.1.0")
|
||||||
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
|
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")
|
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
|
||||||
mark_as_advanced(SYMGEN_PATH)
|
mark_as_advanced(SYMGEN_PATH)
|
||||||
|
|||||||
@@ -37,16 +37,6 @@ function(setup_windows_exports target)
|
|||||||
endif ()
|
endif ()
|
||||||
endforeach ()
|
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
|
# Generate curated exports list from the main binary
|
||||||
set(_def "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.def")
|
set(_def "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.def")
|
||||||
add_custom_command(TARGET ${target} PRE_LINK
|
add_custom_command(TARGET ${target} PRE_LINK
|
||||||
@@ -60,7 +50,6 @@ function(setup_windows_exports target)
|
|||||||
--exclude asan_options
|
--exclude asan_options
|
||||||
--max-exports 58000
|
--max-exports 58000
|
||||||
${_sdk_args}
|
${_sdk_args}
|
||||||
${_forward_args}
|
|
||||||
COMMENT "Generating dusklight exports"
|
COMMENT "Generating dusklight exports"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
target_link_options(${target} PRIVATE "/DEF:${_def}")
|
target_link_options(${target} PRIVATE "/DEF:${_def}")
|
||||||
|
|||||||
+1
-49
@@ -28,7 +28,7 @@ function, read and write data fields, and hook the vast majority of game functio
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Fork the [mod template](../mods/template_mod/), a self-contained CMake project that uses the Dusklight mod SDK.
|
Fork the [mod template](../tools/mod_template/), a self-contained CMake project that uses the Dusklight mod SDK.
|
||||||
|
|
||||||
```
|
```
|
||||||
my_mod/
|
my_mod/
|
||||||
@@ -409,54 +409,6 @@ existing documents restyle immediately, and future ones pick it up when created.
|
|||||||
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
|
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
|
||||||
unless changing host UI is intentional.
|
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
|
## Hooking Game Functions
|
||||||
|
|||||||
Vendored
+1
-1
Submodule extern/aurora updated: 1dde08fa0d...0a5a5d90ef
@@ -1562,11 +1562,9 @@ set(DUSK_FILES
|
|||||||
src/dusk/mods/loader/loader.hpp
|
src/dusk/mods/loader/loader.hpp
|
||||||
src/dusk/mods/loader/native_module.cpp
|
src/dusk/mods/loader/native_module.cpp
|
||||||
src/dusk/mods/loader/native_module.hpp
|
src/dusk/mods/loader/native_module.hpp
|
||||||
src/dusk/mods/svc/camera.cpp
|
|
||||||
src/dusk/mods/svc/config.cpp
|
src/dusk/mods/svc/config.cpp
|
||||||
src/dusk/mods/svc/config.hpp
|
src/dusk/mods/svc/config.hpp
|
||||||
src/dusk/mods/svc/game.cpp
|
src/dusk/mods/svc/game.cpp
|
||||||
src/dusk/mods/svc/gfx.cpp
|
|
||||||
src/dusk/mods/svc/hook.cpp
|
src/dusk/mods/svc/hook.cpp
|
||||||
src/dusk/mods/svc/host.cpp
|
src/dusk/mods/svc/host.cpp
|
||||||
src/dusk/mods/svc/log.cpp
|
src/dusk/mods/svc/log.cpp
|
||||||
|
|||||||
@@ -1049,7 +1049,7 @@ public:
|
|||||||
|
|
||||||
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
|
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
|
||||||
|
|
||||||
DUSK_GAME_EXTERN dComIfG_inf_c g_dComIfG_gameInfo;
|
extern dComIfG_inf_c g_dComIfG_gameInfo;
|
||||||
extern GXColor g_blackColor;
|
extern GXColor g_blackColor;
|
||||||
extern GXColor g_clearColor;
|
extern GXColor g_clearColor;
|
||||||
extern GXColor g_whiteColor;
|
extern GXColor g_whiteColor;
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ public:
|
|||||||
/* 0x130C */ u8 staffroll_next_timer;
|
/* 0x130C */ u8 staffroll_next_timer;
|
||||||
}; // Size: 0x1310
|
}; // Size: 0x1310
|
||||||
|
|
||||||
DUSK_GAME_EXTERN dScnKy_env_light_c g_env_light;
|
extern dScnKy_env_light_c g_env_light;
|
||||||
|
|
||||||
STATIC_ASSERT(sizeof(dScnKy_env_light_c) == 4880);
|
STATIC_ASSERT(sizeof(dScnKy_env_light_c) == 4880);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -266,6 +266,7 @@ struct UserSettings {
|
|||||||
ConfigVar<bool> fastSpinner;
|
ConfigVar<bool> fastSpinner;
|
||||||
ConfigVar<MagicArmorMode> armorRupeeDrain;
|
ConfigVar<MagicArmorMode> armorRupeeDrain;
|
||||||
ConfigVar<bool> invincibleEnemies;
|
ConfigVar<bool> invincibleEnemies;
|
||||||
|
ConfigVar<bool> easyQuickSpin;
|
||||||
|
|
||||||
// Technical
|
// Technical
|
||||||
ConfigVar<bool> restoreWiiGlitches;
|
ConfigVar<bool> restoreWiiGlitches;
|
||||||
|
|||||||
@@ -37,6 +37,5 @@ struct SpeedrunInfo {
|
|||||||
extern SpeedrunInfo m_speedrunInfo;
|
extern SpeedrunInfo m_speedrunInfo;
|
||||||
|
|
||||||
void resetForSpeedrunMode();
|
void resetForSpeedrunMode();
|
||||||
void restoreFromSpeedrunMode();
|
|
||||||
|
|
||||||
} // namespace dusk
|
} // namespace dusk
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -4,8 +4,6 @@
|
|||||||
#include "JSystem/J3DGraphBase/J3DShapeDraw.h"
|
#include "JSystem/J3DGraphBase/J3DShapeDraw.h"
|
||||||
#include "JSystem/J3DAssert.h"
|
#include "JSystem/J3DAssert.h"
|
||||||
#include "JSystem/J3DGraphBase/J3DFifo.h"
|
#include "JSystem/J3DGraphBase/J3DFifo.h"
|
||||||
#include "JSystem/JMath/JMath.h"
|
|
||||||
#include "global.h"
|
|
||||||
#include <mtx.h>
|
#include <mtx.h>
|
||||||
#include "dusk/endian_gx.hpp"
|
#include "dusk/endian_gx.hpp"
|
||||||
|
|
||||||
@@ -204,7 +202,7 @@ public:
|
|||||||
|
|
||||||
static void resetVcdVatCache() { sOldVcdVatCmd = NULL; }
|
static void resetVcdVatCache() { sOldVcdVatCmd = NULL; }
|
||||||
|
|
||||||
static DUSK_GAME_DATA void* sOldVcdVatCmd;
|
static void* sOldVcdVatCmd;
|
||||||
static bool sEnvelopeFlag;
|
static bool sEnvelopeFlag;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
#include "JSystem/JMath/JMath.h"
|
#include "JSystem/JMath/JMath.h"
|
||||||
#include "dusk/frame_interpolation.h"
|
#include "dusk/frame_interpolation.h"
|
||||||
#include "dusk/endian.h"
|
#include "dusk/endian.h"
|
||||||
#include "global.h"
|
|
||||||
|
|
||||||
enum J3DSysDrawBuf {
|
enum J3DSysDrawBuf {
|
||||||
/* 0x0 */ J3DSysDrawBuf_Opa,
|
/* 0x0 */ J3DSysDrawBuf_Opa,
|
||||||
@@ -209,6 +208,6 @@ struct J3DSys {
|
|||||||
};
|
};
|
||||||
|
|
||||||
extern u32 j3dDefaultViewNo;
|
extern u32 j3dDefaultViewNo;
|
||||||
DUSK_GAME_EXTERN J3DSys j3dSys;
|
extern J3DSys j3dSys;
|
||||||
|
|
||||||
#endif /* J3DSYS_H */
|
#endif /* J3DSYS_H */
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// 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));
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
// 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));
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// 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));
|
|
||||||
}
|
|
||||||
@@ -1,931 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
// 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
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "com.example.mod",
|
|
||||||
"name": "Template Mod",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "You",
|
|
||||||
"description": "An example Dusklight mod"
|
|
||||||
}
|
|
||||||
@@ -121,7 +121,6 @@ mod-entry .mod-entry-desc {
|
|||||||
color: rgba(224, 219, 200, 65%);
|
color: rgba(224, 219, 200, 65%);
|
||||||
max-height: 2.6em;
|
max-height: 2.6em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod-entry .mod-entry-sub {
|
mod-entry .mod-entry-sub {
|
||||||
|
|||||||
+2
-7
@@ -1,7 +1,7 @@
|
|||||||
# Dusklight Mod SDK entry point
|
# Dusklight Mod SDK entry point
|
||||||
#
|
#
|
||||||
# Provides game/service headers, compile definitions, version.h and WebGPU
|
# Provides game/service headers, compile definitions and version.h without
|
||||||
# headers without configuring the full game tree.
|
# configuring the full game tree.
|
||||||
#
|
#
|
||||||
# Usage (from a mod project):
|
# Usage (from a mod project):
|
||||||
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
|
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
|
||||||
@@ -30,11 +30,6 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/DetectVersion.cmake")
|
|||||||
detect_version()
|
detect_version()
|
||||||
configure_version_header()
|
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
|
# Game ABI headers & compile definitions
|
||||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GameABIConfig.cmake")
|
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GameABIConfig.cmake")
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ BOOL daAlink_c::checkCutTypeNoBlur() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool daAlink_c::checkCutTurnInput() const {
|
bool daAlink_c::checkCutTurnInput() const {
|
||||||
|
#if TARGET_PC
|
||||||
|
if (mDoCPd_c::getHoldR(PAD_1) && dusk::getSettings().game.easyQuickSpin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return 0xF800 < abs(field_0x3180);
|
return 0xF800 < abs(field_0x3180);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-16
@@ -37,7 +37,6 @@
|
|||||||
#include "dusk/settings.h"
|
#include "dusk/settings.h"
|
||||||
#include "dusk/frame_interpolation.h"
|
#include "dusk/frame_interpolation.h"
|
||||||
#include "dusk/game_clock.h"
|
#include "dusk/game_clock.h"
|
||||||
static f32 timeScale = 1.0f;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static void GxXFog_set();
|
static void GxXFog_set();
|
||||||
@@ -1800,9 +1799,6 @@ void dScnKy_env_light_c::setLight_palno_get(u8* prev_envr_id_p, u8* next_envr_id
|
|||||||
u8 psel_idx = 0;
|
u8 psel_idx = 0;
|
||||||
int i;
|
int i;
|
||||||
int sp14 = 0;
|
int sp14 = 0;
|
||||||
#if TARGET_PC
|
|
||||||
const f32 timeScale = (pattern_ratio_p == &g_env_light.pat_ratio) ? ::timeScale : 1.0f;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (*init_timer_p != 0) {
|
if (*init_timer_p != 0) {
|
||||||
(*init_timer_p)++;
|
(*init_timer_p)++;
|
||||||
@@ -2136,22 +2132,14 @@ void dScnKy_env_light_c::setLight_palno_get(u8* prev_envr_id_p, u8* next_envr_id
|
|||||||
|
|
||||||
if (g_env_light.mColPatMode == 0) {
|
if (g_env_light.mColPatMode == 0) {
|
||||||
if (pselect_p->change_rate > 0.0f) {
|
if (pselect_p->change_rate > 0.0f) {
|
||||||
#if TARGET_PC
|
|
||||||
*pattern_ratio_p += timeScale * ((1.0f / 30) / pselect_p->change_rate);
|
|
||||||
#else
|
|
||||||
*pattern_ratio_p += (1.0f / 30) / pselect_p->change_rate;
|
*pattern_ratio_p += (1.0f / 30) / pselect_p->change_rate;
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pattern change rate is faster in hyrule field
|
// pattern change rate is faster in hyrule field
|
||||||
if (strcmp(dComIfGp_getStartStageName(), "F_SP121") == 0 &&
|
if (strcmp(dComIfGp_getStartStageName(), "F_SP121") == 0 &&
|
||||||
*prev_pat_p == *next_pat_p)
|
*prev_pat_p == *next_pat_p)
|
||||||
{
|
{
|
||||||
#if TARGET_PC
|
|
||||||
*pattern_ratio_p += timeScale * (1.0f / 15);
|
|
||||||
#else
|
|
||||||
*pattern_ratio_p += (1.0f / 15);
|
*pattern_ratio_p += (1.0f / 15);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*pattern_ratio_p >= 1.0f) {
|
if (*pattern_ratio_p >= 1.0f) {
|
||||||
@@ -2344,10 +2332,6 @@ void dScnKy_env_light_c::setLight() {
|
|||||||
u8 next_pal_start_id;
|
u8 next_pal_start_id;
|
||||||
u8 prev_pal_end_id;
|
u8 prev_pal_end_id;
|
||||||
u8 next_pal_end_id;
|
u8 next_pal_end_id;
|
||||||
#if TARGET_PC
|
|
||||||
const f32 deltaTime = dusk::game_clock::consume_interval(this);
|
|
||||||
timeScale = deltaTime / dusk::game_clock::period_for_original_frames(1.0f);
|
|
||||||
#endif
|
|
||||||
setLight_palno_get(&g_env_light.PrevCol, &g_env_light.UseCol, &g_env_light.wether_pat0,
|
setLight_palno_get(&g_env_light.PrevCol, &g_env_light.UseCol, &g_env_light.wether_pat0,
|
||||||
&g_env_light.wether_pat1, &prev_pal_start_id, &prev_pal_end_id,
|
&g_env_light.wether_pat1, &prev_pal_start_id, &prev_pal_end_id,
|
||||||
&next_pal_start_id, &next_pal_end_id, &color_ratio, &start_pat_pal_id,
|
&next_pal_start_id, &next_pal_end_id, &color_ratio, &start_pat_pal_id,
|
||||||
@@ -2533,6 +2517,8 @@ void dScnKy_env_light_c::setLight() {
|
|||||||
f32 sin = cM_ssin(S_fuwan_sin);
|
f32 sin = cM_ssin(S_fuwan_sin);
|
||||||
|
|
||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
|
const f32 deltaTime = dusk::game_clock::consume_interval(this);
|
||||||
|
const f32 timeScale = deltaTime / dusk::game_clock::period_for_original_frames(1.0f);
|
||||||
S_fuwan_sin += (s16)((cM_rndF(2000.0f) + 500) * timeScale);
|
S_fuwan_sin += (s16)((cM_rndF(2000.0f) + 500) * timeScale);
|
||||||
#else
|
#else
|
||||||
S_fuwan_sin += (s16)cM_rndF(2000.0f) + 500;
|
S_fuwan_sin += (s16)cM_rndF(2000.0f) + 500;
|
||||||
|
|||||||
+9
-31
@@ -27,15 +27,6 @@
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
#include "dusk/memory.h"
|
#include "dusk/memory.h"
|
||||||
#include "dusk/settings.h"
|
#include "dusk/settings.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Reads the user HUD scale setting, clamped to a safe range.
|
|
||||||
f32 dGetUserHudScale() {
|
|
||||||
return std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int dMeter2_c::_create() {
|
int dMeter2_c::_create() {
|
||||||
@@ -678,7 +669,9 @@ void dMeter2_c::moveLife() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
const f32 lifeGaugeScale = g_drawHIO.mLifeParentScale * dGetUserHudScale();
|
const f32 lifeGaugeScale =
|
||||||
|
g_drawHIO.mLifeParentScale *
|
||||||
|
std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
|
||||||
#else
|
#else
|
||||||
const f32 lifeGaugeScale = g_drawHIO.mLifeParentScale;
|
const f32 lifeGaugeScale = g_drawHIO.mLifeParentScale;
|
||||||
#endif
|
#endif
|
||||||
@@ -1115,13 +1108,8 @@ void dMeter2_c::moveRupee() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if TARGET_PC
|
if (mRupeeKeyScale != g_drawHIO.mRupeeKeyScale) {
|
||||||
const f32 rupeeKeyScale = g_drawHIO.mRupeeKeyScale * dGetUserHudScale();
|
mRupeeKeyScale = g_drawHIO.mRupeeKeyScale;
|
||||||
#else
|
|
||||||
const f32 rupeeKeyScale = g_drawHIO.mRupeeKeyScale;
|
|
||||||
#endif
|
|
||||||
if (mRupeeKeyScale != rupeeKeyScale) {
|
|
||||||
mRupeeKeyScale = rupeeKeyScale;
|
|
||||||
draw_rupee = true;
|
draw_rupee = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1219,13 +1207,8 @@ void dMeter2_c::moveKey() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if TARGET_PC
|
if (mKeyScale != g_drawHIO.mKeyScale) {
|
||||||
const f32 keyScale = g_drawHIO.mKeyScale * dGetUserHudScale();
|
mKeyScale = g_drawHIO.mKeyScale;
|
||||||
#else
|
|
||||||
const f32 keyScale = g_drawHIO.mKeyScale;
|
|
||||||
#endif
|
|
||||||
if (mKeyScale != keyScale) {
|
|
||||||
mKeyScale = keyScale;
|
|
||||||
draw_key = true;
|
draw_key = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2156,13 +2139,8 @@ void dMeter2_c::moveButtonCross() {
|
|||||||
draw_cross = true;
|
draw_cross = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if TARGET_PC
|
if (mButtonCrossScale != g_drawHIO.mButtonCrossScale) {
|
||||||
const f32 buttonCrossScale = g_drawHIO.mButtonCrossScale * dGetUserHudScale();
|
mButtonCrossScale = g_drawHIO.mButtonCrossScale;
|
||||||
#else
|
|
||||||
const f32 buttonCrossScale = g_drawHIO.mButtonCrossScale;
|
|
||||||
#endif
|
|
||||||
if (mButtonCrossScale != buttonCrossScale) {
|
|
||||||
mButtonCrossScale = buttonCrossScale;
|
|
||||||
draw_cross = true;
|
draw_cross = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ void dAnchorHudScale(CPaneMgr* i_pane, HudCorner i_corner, f32* io_x, f32* io_y,
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap* mp_heap) {
|
dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap* mp_heap) {
|
||||||
OS_REPORT("enter dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap *mp_heap)\n");
|
OS_REPORT("enter dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap *mp_heap)\n");
|
||||||
|
|
||||||
@@ -2782,7 +2781,7 @@ void dMeter2Draw_c::drawButtonCross(f32 i_posX, f32 i_posY) {
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
f32 buttonCrossPosX = i_posX;
|
f32 buttonCrossPosX = i_posX;
|
||||||
f32 buttonCrossPosY = i_posY;
|
f32 buttonCrossPosY = i_posY;
|
||||||
dAnchorHudScale(mpButtonCrossParent, HudCorner::BottomLeft, &buttonCrossPosX, &buttonCrossPosY);
|
dAnchorHudScale(mpButtonCrossParent, HudCorner::TopLeft, &buttonCrossPosX, &buttonCrossPosY);
|
||||||
mpButtonCrossParent->paneTrans(buttonCrossPosX, buttonCrossPosY);
|
mpButtonCrossParent->paneTrans(buttonCrossPosX, buttonCrossPosY);
|
||||||
#else
|
#else
|
||||||
mpButtonCrossParent->paneTrans(i_posX, i_posY);
|
mpButtonCrossParent->paneTrans(i_posX, i_posY);
|
||||||
|
|||||||
+3
-11
@@ -24,15 +24,6 @@
|
|||||||
|
|
||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
#include "dusk/action_bindings.h"
|
#include "dusk/action_bindings.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Reads the user HUD scale setting, clamped to a safe range.
|
|
||||||
f32 dGetUserHudScale() {
|
|
||||||
return std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if (PLATFORM_WII || PLATFORM_SHIELD)
|
#if (PLATFORM_WII || PLATFORM_SHIELD)
|
||||||
@@ -335,7 +326,7 @@ f32 dMeterMap_c::getMapDispEdgeTop() {
|
|||||||
mMap->getTexelPerCm() * (mMap->getPackZ() + -mMap->getPackPlusZ()) -
|
mMap->getTexelPerCm() * (mMap->getPackZ() + -mMap->getPackPlusZ()) -
|
||||||
mMap->getTopEdgePlus();
|
mMap->getTopEdgePlus();
|
||||||
}
|
}
|
||||||
f32 rv = getMapDispEdgeBottomY_Layout() - tmp IF_DUSK(* dGetUserHudScale());
|
f32 rv = getMapDispEdgeBottomY_Layout() - tmp;
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,7 +637,8 @@ void dMeterMap_c::draw() {
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
// Scale the minimap with the user HUD scale and shift down so its bottom-left
|
// Scale the minimap with the user HUD scale and shift down so its bottom-left
|
||||||
// corner stays anchored to the same screen position as at scale 1.0.
|
// corner stays anchored to the same screen position as at scale 1.0.
|
||||||
const f32 userHudScale = dGetUserHudScale();
|
const f32 userHudScale =
|
||||||
|
std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
|
||||||
const f32 scaledSizeX = sizeX * userHudScale;
|
const f32 scaledSizeX = sizeX * userHudScale;
|
||||||
const f32 scaledSizeY = sizeY * userHudScale;
|
const f32 scaledSizeY = sizeY * userHudScale;
|
||||||
const f32 mapBottomShift = sizeY - scaledSizeY;
|
const f32 mapBottomShift = sizeY - scaledSizeY;
|
||||||
|
|||||||
@@ -646,19 +646,6 @@ static const MirrorMsgOverride mirrorMsgOverrides[] = {
|
|||||||
{0x14ca, 0x3bda},
|
{0x14ca, 0x3bda},
|
||||||
{0x470, 0x493},
|
{0x470, 0x493},
|
||||||
{0x473, 0x492},
|
{0x473, 0x492},
|
||||||
{0x1f41, 0x4651},
|
|
||||||
{0x1f42, 0x4652},
|
|
||||||
{0x0847, 0x0870},
|
|
||||||
{0x0d5c, 0x0d65},
|
|
||||||
{0x0a97, 0x0a98},
|
|
||||||
{0x0327, 0x12ba},
|
|
||||||
{0x0328, 0x12bb},
|
|
||||||
{0x1534, 0x3c44},
|
|
||||||
{0x1536, 0x3c46},
|
|
||||||
{0x1557, 0x3c67},
|
|
||||||
{0x1b88, 0x4298},
|
|
||||||
{0x14c8, 0x3bd8},
|
|
||||||
{0x151b, 0x3c2b},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static u32 getMirrorMsgOverride(u32 msgId) {
|
static u32 getMirrorMsgOverride(u32 msgId) {
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
#include "dusk/mods/loader/loader.hpp"
|
|
||||||
#include "mods/svc/camera.h"
|
|
||||||
|
|
||||||
#include "f_op/f_op_view.h"
|
|
||||||
#include "m_Do/m_Do_mtx.h"
|
|
||||||
|
|
||||||
#include <aurora/gfx.hpp>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace dusk::mods::svc {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void to_column_major(const Mtx44 in, float out[16]) {
|
|
||||||
for (int c = 0; c < 4; ++c) {
|
|
||||||
for (int r = 0; r < 4; ++r) {
|
|
||||||
out[c * 4 + r] = in[r][c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void store_affine(const Mtx in, float out[16]) {
|
|
||||||
for (int c = 0; c < 4; ++c) {
|
|
||||||
for (int r = 0; r < 3; ++r) {
|
|
||||||
out[c * 4 + r] = in[r][c];
|
|
||||||
}
|
|
||||||
out[c * 4 + 3] = 0.0f;
|
|
||||||
}
|
|
||||||
out[15] = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* affine * 4x4; operand-order mirror of cMtx_concatProjView */
|
|
||||||
void concat_affine_proj(const Mtx a, const Mtx44 b, Mtx44 out) {
|
|
||||||
for (int r = 0; r < 3; ++r) {
|
|
||||||
for (int c = 0; c < 4; ++c) {
|
|
||||||
out[r][c] =
|
|
||||||
a[r][0] * b[0][c] + a[r][1] * b[1][c] + a[r][2] * b[2][c] + a[r][3] * b[3][c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::memcpy(out[3], b[3], sizeof(f32) * 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult snapshot_view(const view_class* view, CameraInfo* outInfo) {
|
|
||||||
if (view == nullptr || !(view->near_ > 0.0f) || !(view->far_ > view->near_) ||
|
|
||||||
!(view->fovy > 0.0f) || view->fovy >= 180.0f || !(view->aspect > 0.0f))
|
|
||||||
{
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build from the GXSetProjection values
|
|
||||||
const f32 p00 = view->projMtx[0][0];
|
|
||||||
const f32 p02 = view->projMtx[0][2];
|
|
||||||
const f32 p11 = view->projMtx[1][1];
|
|
||||||
const f32 p12 = view->projMtx[1][2];
|
|
||||||
const f32 p22 = view->projMtx[2][2];
|
|
||||||
const f32 p23 = view->projMtx[2][3];
|
|
||||||
if (view->projMtx[3][2] != -1.0f || p00 == 0.0f || p11 == 0.0f || p23 == 0.0f) {
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebGPU-convention projection + Aurora reversed Z
|
|
||||||
const bool reversedZ = aurora::gfx::uses_reversed_z();
|
|
||||||
const f32 e = reversedZ ? -p22 : p22 - 1.0f;
|
|
||||||
const f32 f = reversedZ ? -p23 : p23;
|
|
||||||
Mtx44 proj{};
|
|
||||||
proj[0][0] = p00;
|
|
||||||
proj[0][2] = p02;
|
|
||||||
proj[1][1] = p11;
|
|
||||||
proj[1][2] = p12;
|
|
||||||
proj[2][2] = e;
|
|
||||||
proj[2][3] = f;
|
|
||||||
proj[3][2] = -1.0f;
|
|
||||||
|
|
||||||
// Analytic inverse of the sparse perspective form
|
|
||||||
Mtx44 invProj{};
|
|
||||||
invProj[0][0] = 1.0f / p00;
|
|
||||||
invProj[0][3] = p02 / p00;
|
|
||||||
invProj[1][1] = 1.0f / p11;
|
|
||||||
invProj[1][3] = p12 / p11;
|
|
||||||
invProj[2][3] = -1.0f;
|
|
||||||
invProj[3][2] = 1.0f / f;
|
|
||||||
invProj[3][3] = e / f;
|
|
||||||
|
|
||||||
Mtx44 projWorld;
|
|
||||||
cMtx_concatProjView(proj, view->viewMtx, projWorld);
|
|
||||||
Mtx44 worldProj;
|
|
||||||
concat_affine_proj(view->invViewMtx, invProj, worldProj);
|
|
||||||
|
|
||||||
store_affine(view->viewMtx, outInfo->view_from_world);
|
|
||||||
store_affine(view->invViewMtx, outInfo->world_from_view);
|
|
||||||
to_column_major(proj, outInfo->proj_from_view);
|
|
||||||
to_column_major(invProj, outInfo->view_from_proj);
|
|
||||||
to_column_major(projWorld, outInfo->proj_from_world);
|
|
||||||
to_column_major(worldProj, outInfo->world_from_proj);
|
|
||||||
|
|
||||||
outInfo->eye[0] = view->lookat.eye.x;
|
|
||||||
outInfo->eye[1] = view->lookat.eye.y;
|
|
||||||
outInfo->eye[2] = view->lookat.eye.z;
|
|
||||||
outInfo->fovy = view->fovy;
|
|
||||||
outInfo->aspect = view->aspect;
|
|
||||||
outInfo->near_plane = view->near_;
|
|
||||||
outInfo->far_plane = view->far_;
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult camera_get_camera_from_view(
|
|
||||||
ModContext* context, const void* gameView, CameraInfo* outInfo) {
|
|
||||||
if (outInfo == nullptr || outInfo->struct_size < sizeof(CameraInfo) ||
|
|
||||||
mod_from_context(context) == nullptr || gameView == nullptr)
|
|
||||||
{
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
const uint32_t structSize = outInfo->struct_size;
|
|
||||||
std::memset(outInfo, 0, sizeof(CameraInfo));
|
|
||||||
outInfo->struct_size = structSize;
|
|
||||||
return snapshot_view(static_cast<const view_class*>(gameView), outInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr CameraService s_cameraService{
|
|
||||||
.header = SERVICE_HEADER(CameraService, CAMERA_SERVICE_MAJOR, CAMERA_SERVICE_MINOR),
|
|
||||||
.get_camera = camera_get_camera_from_view,
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
constinit const ServiceModule g_cameraModule{
|
|
||||||
.id = CAMERA_SERVICE_ID,
|
|
||||||
.majorVersion = CAMERA_SERVICE_MAJOR,
|
|
||||||
.minorVersion = CAMERA_SERVICE_MINOR,
|
|
||||||
.service = &s_cameraService,
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace dusk::mods::svc
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
|
|
||||||
#include "registry.hpp"
|
#include "registry.hpp"
|
||||||
#include "slot_map.hpp"
|
|
||||||
|
|
||||||
#include "aurora/lib/logging.hpp"
|
#include "aurora/lib/logging.hpp"
|
||||||
#include "dusk/config.hpp"
|
#include "dusk/config.hpp"
|
||||||
@@ -16,6 +15,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace dusk::mods::svc {
|
namespace dusk::mods::svc {
|
||||||
@@ -23,22 +23,25 @@ namespace {
|
|||||||
|
|
||||||
aurora::Module Log("dusk::mods::config");
|
aurora::Module Log("dusk::mods::config");
|
||||||
|
|
||||||
enum class ConfigSlotKind : uint8_t {
|
struct ModConfigVarEntry {
|
||||||
Var,
|
uint64_t handle = 0;
|
||||||
Subscription,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ConfigSlot {
|
|
||||||
ConfigSlotKind kind = ConfigSlotKind::Var;
|
|
||||||
// Var payload
|
|
||||||
ConfigVarType type = CONFIG_VAR_BOOL;
|
ConfigVarType type = CONFIG_VAR_BOOL;
|
||||||
std::unique_ptr<config::ConfigVarBase> var;
|
std::unique_ptr<config::ConfigVarBase> var;
|
||||||
// Subscription payload
|
};
|
||||||
|
|
||||||
|
struct ModConfigSubscription {
|
||||||
|
uint64_t handle = 0;
|
||||||
uint64_t varHandle = 0;
|
uint64_t varHandle = 0;
|
||||||
config::Subscription coreSubscription = 0;
|
config::Subscription coreSubscription = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
SlotMap<ConfigSlot> s_slots;
|
struct ModConfigRecord {
|
||||||
|
std::vector<ModConfigVarEntry> vars;
|
||||||
|
std::vector<ModConfigSubscription> subscriptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<const LoadedMod*, ModConfigRecord> s_modConfig;
|
||||||
|
uint64_t s_nextHandle = 1;
|
||||||
bool s_dirty = false;
|
bool s_dirty = false;
|
||||||
std::chrono::steady_clock::time_point s_lastSave{};
|
std::chrono::steady_clock::time_point s_lastSave{};
|
||||||
constexpr std::chrono::seconds kSaveDebounce{2};
|
constexpr std::chrono::seconds kSaveDebounce{2};
|
||||||
@@ -127,13 +130,17 @@ bool valid_var_fragment(const char* name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config::ConfigVarBase* find_var(LoadedMod& mod, const uint64_t handle, uint32_t expectedType) {
|
config::ConfigVarBase* find_var(LoadedMod& mod, const uint64_t handle, uint32_t expectedType) {
|
||||||
const auto* entry = s_slots.find_owned(handle, mod);
|
const auto recordIt = s_modConfig.find(&mod);
|
||||||
if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var ||
|
if (recordIt == s_modConfig.end()) {
|
||||||
entry->value.type != expectedType)
|
|
||||||
{
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return entry->value.var.get();
|
const auto& vars = recordIt->second.vars;
|
||||||
|
const auto entry =
|
||||||
|
std::ranges::find_if(vars, [&](const auto& e) { return e.handle == handle; });
|
||||||
|
if (entry == vars.end() || entry->type != expectedType) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return entry->var.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
@@ -147,17 +154,17 @@ ConfigVar<T>* find_typed_var(ModContext* context, ConfigVarHandle handle, uint32
|
|||||||
}
|
}
|
||||||
|
|
||||||
void config_remove_mod(LoadedMod& mod) {
|
void config_remove_mod(LoadedMod& mod) {
|
||||||
const auto entries = s_slots.take_all(mod);
|
const auto it = s_modConfig.find(&mod);
|
||||||
for (const auto& entry : entries) {
|
if (it == s_modConfig.end()) {
|
||||||
if (entry.value.kind == ConfigSlotKind::Subscription) {
|
return;
|
||||||
config::unsubscribe(entry.value.coreSubscription);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const auto& entry : entries) {
|
for (const auto& sub : it->second.subscriptions) {
|
||||||
if (entry.value.kind == ConfigSlotKind::Var) {
|
config::unsubscribe(sub.coreSubscription);
|
||||||
config::unregister(*entry.value.var);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
for (const auto& entry : it->second.vars) {
|
||||||
|
config::unregister(*entry.var);
|
||||||
|
}
|
||||||
|
s_modConfig.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
ModResult config_register_var(
|
ModResult config_register_var(
|
||||||
@@ -183,16 +190,16 @@ ModResult config_register_var(
|
|||||||
std::unique_ptr<config::ConfigVarBase> var;
|
std::unique_ptr<config::ConfigVarBase> var;
|
||||||
switch (desc->type) {
|
switch (desc->type) {
|
||||||
case CONFIG_VAR_BOOL:
|
case CONFIG_VAR_BOOL:
|
||||||
var = std::make_unique<ConfigVar<bool>>(fullName, desc->default_bool);
|
var = std::make_unique<ConfigVar<bool> >(fullName, desc->default_bool);
|
||||||
break;
|
break;
|
||||||
case CONFIG_VAR_INT:
|
case CONFIG_VAR_INT:
|
||||||
var = std::make_unique<ConfigVar<s64>>(fullName, desc->default_int);
|
var = std::make_unique<ConfigVar<s64> >(fullName, desc->default_int);
|
||||||
break;
|
break;
|
||||||
case CONFIG_VAR_FLOAT:
|
case CONFIG_VAR_FLOAT:
|
||||||
var = std::make_unique<ConfigVar<f64>>(fullName, desc->default_float);
|
var = std::make_unique<ConfigVar<f64> >(fullName, desc->default_float);
|
||||||
break;
|
break;
|
||||||
case CONFIG_VAR_STRING:
|
case CONFIG_VAR_STRING:
|
||||||
var = std::make_unique<ConfigVar<std::string>>(
|
var = std::make_unique<ConfigVar<std::string> >(
|
||||||
fullName, desc->default_string != nullptr ? desc->default_string : "");
|
fullName, desc->default_string != nullptr ? desc->default_string : "");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -204,10 +211,13 @@ ModResult config_register_var(
|
|||||||
// reads the value right after registration anyway.
|
// reads the value right after registration anyway.
|
||||||
config::Register(*var);
|
config::Register(*var);
|
||||||
|
|
||||||
const auto handle = s_slots.emplace(
|
auto& record = s_modConfig[mod];
|
||||||
*mod, ConfigSlot{.kind = ConfigSlotKind::Var, .type = desc->type, .var = std::move(var)});
|
auto& entry = record.vars.emplace_back();
|
||||||
|
entry.handle = s_nextHandle++;
|
||||||
|
entry.type = desc->type;
|
||||||
|
entry.var = std::move(var);
|
||||||
if (outHandle != nullptr) {
|
if (outHandle != nullptr) {
|
||||||
*outHandle = handle;
|
*outHandle = entry.handle;
|
||||||
}
|
}
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
@@ -217,26 +227,29 @@ ModResult config_unregister_var(ModContext* context, ConfigVarHandle var) {
|
|||||||
if (mod == nullptr || var == 0) {
|
if (mod == nullptr || var == 0) {
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
const auto* entry = s_slots.find_owned(var, *mod);
|
const auto recordIt = s_modConfig.find(mod);
|
||||||
if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var) {
|
if (recordIt != s_modConfig.end()) {
|
||||||
Log.error("[{}] config unregister failed: unknown handle {}", mod->metadata.id, var);
|
auto& record = recordIt->second;
|
||||||
return MOD_INVALID_ARGUMENT;
|
const auto entry =
|
||||||
}
|
std::ranges::find_if(record.vars, [&](const auto& e) { return e.handle == var; });
|
||||||
|
if (entry != record.vars.end()) {
|
||||||
|
std::erase_if(record.subscriptions, [&](const ModConfigSubscription& sub) {
|
||||||
|
if (sub.varHandle != var) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
config::unsubscribe(sub.coreSubscription);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Only the owning mod can (currently) subscribe to a var
|
// The persisted value is stashed and restored by a future registration of the
|
||||||
std::vector<uint64_t> bound;
|
// same name.
|
||||||
s_slots.for_each([&](const uint64_t handle, const auto& e) {
|
config::unregister(*entry->var);
|
||||||
if (e.value.kind == ConfigSlotKind::Subscription && e.value.varHandle == var) {
|
record.vars.erase(entry);
|
||||||
bound.push_back(handle);
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
for (const auto handle : bound) {
|
|
||||||
config::unsubscribe(s_slots.take(handle)->value.coreSubscription);
|
|
||||||
}
|
}
|
||||||
|
Log.error("[{}] config unregister failed: unknown handle {}", mod->metadata.id, var);
|
||||||
// The persisted value is stashed and restored by a future registration of the same name.
|
return MOD_INVALID_ARGUMENT;
|
||||||
config::unregister(*s_slots.take(var)->value.var);
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ModResult config_get_bool(ModContext* context, ConfigVarHandle var, bool* outValue) {
|
ModResult config_get_bool(ModContext* context, ConfigVarHandle var, bool* outValue) {
|
||||||
@@ -348,13 +361,22 @@ ModResult config_subscribe(ModContext* context, ConfigVarHandle var, ConfigChang
|
|||||||
if (mod == nullptr || var == 0 || callback == nullptr) {
|
if (mod == nullptr || var == 0 || callback == nullptr) {
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
const auto* entry = s_slots.find_owned(var, *mod);
|
const auto recordIt = s_modConfig.find(mod);
|
||||||
if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var) {
|
if (recordIt == s_modConfig.end()) {
|
||||||
|
return MOD_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
auto& record = recordIt->second;
|
||||||
|
const auto entry =
|
||||||
|
std::ranges::find_if(record.vars, [&](const auto& e) { return e.handle == var; });
|
||||||
|
if (entry == record.vars.end()) {
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto coreSubscription = config::subscribe(entry->value.var->getName(),
|
auto& sub = record.subscriptions.emplace_back();
|
||||||
[modPtr = mod, callback, userData, varHandle = var, type = entry->value.type](
|
sub.handle = s_nextHandle++;
|
||||||
|
sub.varHandle = var;
|
||||||
|
sub.coreSubscription = config::subscribe(entry->var->getName(),
|
||||||
|
[modPtr = mod, callback, userData, varHandle = var, type = entry->type](
|
||||||
config::ConfigVarBase& varBase, const void* previous) {
|
config::ConfigVarBase& varBase, const void* previous) {
|
||||||
const ConfigVarValue previousValue = translate_previous(type, previous);
|
const ConfigVarValue previousValue = translate_previous(type, previous);
|
||||||
std::string stringStorage;
|
std::string stringStorage;
|
||||||
@@ -368,13 +390,8 @@ ModResult config_subscribe(ModContext* context, ConfigVarHandle var, ConfigChang
|
|||||||
fail_mod(*modPtr, MOD_ERROR, "Unknown exception in config change callback");
|
fail_mod(*modPtr, MOD_ERROR, "Unknown exception in config change callback");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const auto handle = s_slots.emplace(*mod, ConfigSlot{
|
|
||||||
.kind = ConfigSlotKind::Subscription,
|
|
||||||
.varHandle = var,
|
|
||||||
.coreSubscription = coreSubscription,
|
|
||||||
});
|
|
||||||
if (outHandle != nullptr) {
|
if (outHandle != nullptr) {
|
||||||
*outHandle = handle;
|
*outHandle = sub.handle;
|
||||||
}
|
}
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
@@ -384,14 +401,19 @@ ModResult config_unsubscribe(ModContext* context, ConfigSubscriptionHandle handl
|
|||||||
if (mod == nullptr || handle == 0) {
|
if (mod == nullptr || handle == 0) {
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
const auto* entry = s_slots.find_owned(handle, *mod);
|
const auto recordIt = s_modConfig.find(mod);
|
||||||
if (entry == nullptr || entry->value.kind != ConfigSlotKind::Subscription) {
|
if (recordIt != s_modConfig.end()) {
|
||||||
Log.error("[{}] config unsubscribe failed: unknown handle {}", mod->metadata.id, handle);
|
auto& subscriptions = recordIt->second.subscriptions;
|
||||||
return MOD_INVALID_ARGUMENT;
|
const auto sub =
|
||||||
|
std::ranges::find_if(subscriptions, [&](const auto& s) { return s.handle == handle; });
|
||||||
|
if (sub != subscriptions.end()) {
|
||||||
|
config::unsubscribe(sub->coreSubscription);
|
||||||
|
subscriptions.erase(sub);
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
config::unsubscribe(entry->value.coreSubscription);
|
Log.error("[{}] config unsubscribe failed: unknown handle {}", mod->metadata.id, handle);
|
||||||
s_slots.erase(handle);
|
return MOD_INVALID_ARGUMENT;
|
||||||
return MOD_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr ConfigService s_configService{
|
constexpr ConfigService s_configService{
|
||||||
|
|||||||
@@ -1,798 +0,0 @@
|
|||||||
#include "registry.hpp"
|
|
||||||
#include "slot_map.hpp"
|
|
||||||
|
|
||||||
#include "aurora/lib/logging.hpp"
|
|
||||||
#include "dusk/gfx.hpp"
|
|
||||||
#include "dusk/mods/loader/loader.hpp"
|
|
||||||
#include "mods/svc/gfx.h"
|
|
||||||
|
|
||||||
#include <aurora/gfx.hpp>
|
|
||||||
#include <fmt/format.h>
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <exception>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace dusk::mods {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
aurora::Module Log("dusk::mods::gfx");
|
|
||||||
|
|
||||||
enum class GfxSlotKind : uint8_t {
|
|
||||||
DrawType,
|
|
||||||
StageHook,
|
|
||||||
ComputeType,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class GfxStreamBuffer : uint8_t {
|
|
||||||
Verts,
|
|
||||||
Indices,
|
|
||||||
Uniform,
|
|
||||||
Storage,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct GfxSlot {
|
|
||||||
GfxSlotKind kind = GfxSlotKind::DrawType;
|
|
||||||
ModContext* ownerContext = nullptr;
|
|
||||||
std::string ownerId;
|
|
||||||
void* userData = nullptr;
|
|
||||||
|
|
||||||
GfxDrawFn drawFn = nullptr;
|
|
||||||
aurora::gfx::DrawTypeId auroraDrawId = aurora::gfx::InvalidDrawType;
|
|
||||||
|
|
||||||
GfxStageFn stageFn = nullptr;
|
|
||||||
GfxStage stage = GFX_STAGE_SCENE_AFTER_TERRAIN;
|
|
||||||
|
|
||||||
GfxComputeFn computeFn = nullptr;
|
|
||||||
aurora::gfx::EncoderTaskId auroraTaskId = aurora::gfx::InvalidEncoderTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct WorkerFailure {
|
|
||||||
std::string modId;
|
|
||||||
std::string message;
|
|
||||||
std::vector<aurora::gfx::DrawTypeId> drawIds;
|
|
||||||
std::vector<aurora::gfx::EncoderTaskId> taskIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::mutex s_mutex;
|
|
||||||
using GfxSlotMap = svc::SlotMap<GfxSlot>;
|
|
||||||
GfxSlotMap s_slots;
|
|
||||||
std::vector<WorkerFailure> s_workerFailures;
|
|
||||||
bool s_modOffscreenOpen = false;
|
|
||||||
|
|
||||||
GfxSlotMap::Entry* resolve_entry_locked(uint64_t handle, GfxSlotKind kind) {
|
|
||||||
auto* entry = s_slots.find(handle);
|
|
||||||
if (entry == nullptr || entry->value.kind != kind) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
GfxSlot* resolve_slot_locked(uint64_t handle, GfxSlotKind kind) {
|
|
||||||
auto* entry = resolve_entry_locked(handle, kind);
|
|
||||||
return entry != nullptr ? &entry->value : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
GfxSlot* resolve_owned_slot_locked(LoadedMod& mod, uint64_t handle, GfxSlotKind kind) {
|
|
||||||
auto* entry = s_slots.find_owned(handle, mod);
|
|
||||||
if (entry == nullptr || entry->value.kind != kind) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return &entry->value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void collect_mod_slots_locked(LoadedMod& owner, std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
|
||||||
std::vector<aurora::gfx::EncoderTaskId>& taskIds) {
|
|
||||||
auto entries = s_slots.take_all(owner);
|
|
||||||
for (auto& entry : entries) {
|
|
||||||
const auto& slot = entry.value;
|
|
||||||
if (slot.kind == GfxSlotKind::DrawType && slot.auroraDrawId != aurora::gfx::InvalidDrawType)
|
|
||||||
{
|
|
||||||
drawIds.push_back(slot.auroraDrawId);
|
|
||||||
} else if (slot.kind == GfxSlotKind::ComputeType &&
|
|
||||||
slot.auroraTaskId != aurora::gfx::InvalidEncoderTask)
|
|
||||||
{
|
|
||||||
taskIds.push_back(slot.auroraTaskId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void unregister_aurora_types(const std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
|
||||||
const std::vector<aurora::gfx::EncoderTaskId>& taskIds) {
|
|
||||||
for (const auto id : drawIds) {
|
|
||||||
aurora::gfx::unregister_draw_type(id);
|
|
||||||
}
|
|
||||||
for (const auto id : taskIds) {
|
|
||||||
aurora::gfx::unregister_encoder_task_type(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPassEncoder& pass,
|
|
||||||
const void* payload, size_t payloadSize, void* userdata) {
|
|
||||||
const auto handle = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(userdata));
|
|
||||||
GfxDrawFn fn = nullptr;
|
|
||||||
void* userData = nullptr;
|
|
||||||
ModContext* modContext = nullptr;
|
|
||||||
LoadedMod* owner = nullptr;
|
|
||||||
std::string ownerId;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
auto* entry = resolve_entry_locked(handle, GfxSlotKind::DrawType);
|
|
||||||
if (entry == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto& slot = entry->value;
|
|
||||||
fn = slot.drawFn;
|
|
||||||
userData = slot.userData;
|
|
||||||
modContext = slot.ownerContext;
|
|
||||||
owner = entry->owner;
|
|
||||||
ownerId = slot.ownerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
GfxDrawContext drawContext{
|
|
||||||
.struct_size = sizeof(GfxDrawContext),
|
|
||||||
.device = ctx.device.Get(),
|
|
||||||
.queue = ctx.queue.Get(),
|
|
||||||
.pass = pass.Get(),
|
|
||||||
.vertex_buffer = ctx.vertexBuffer.Get(),
|
|
||||||
.index_buffer = ctx.indexBuffer.Get(),
|
|
||||||
.uniform_buffer = ctx.uniformBuffer.Get(),
|
|
||||||
.storage_buffer = ctx.storageBuffer.Get(),
|
|
||||||
.color_format = static_cast<WGPUTextureFormat>(ctx.colorFormat),
|
|
||||||
.depth_format = static_cast<WGPUTextureFormat>(ctx.depthFormat),
|
|
||||||
.sample_count = ctx.sampleCount,
|
|
||||||
.target_width = ctx.targetWidth,
|
|
||||||
.target_height = ctx.targetHeight,
|
|
||||||
.uses_reversed_z = aurora::gfx::uses_reversed_z(),
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string failure;
|
|
||||||
try {
|
|
||||||
fn(modContext, &drawContext, payload, payloadSize, userData);
|
|
||||||
return;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
failure = fmt::format("exception in gfx draw callback: {}", e.what());
|
|
||||||
} catch (...) {
|
|
||||||
failure = "unknown exception in gfx draw callback";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
WorkerFailure record{
|
|
||||||
.modId = std::move(ownerId),
|
|
||||||
.message = std::move(failure),
|
|
||||||
};
|
|
||||||
collect_mod_slots_locked(*owner, record.drawIds, record.taskIds);
|
|
||||||
s_workerFailures.push_back(std::move(record));
|
|
||||||
}
|
|
||||||
|
|
||||||
void compute_trampoline(const aurora::gfx::EncoderTaskContext& ctx, const wgpu::CommandEncoder& cmd,
|
|
||||||
const void* payload, size_t payloadSize, void* userdata) {
|
|
||||||
const auto handle = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(userdata));
|
|
||||||
GfxComputeFn fn = nullptr;
|
|
||||||
void* userData = nullptr;
|
|
||||||
ModContext* modContext = nullptr;
|
|
||||||
LoadedMod* owner = nullptr;
|
|
||||||
std::string ownerId;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
auto* entry = resolve_entry_locked(handle, GfxSlotKind::ComputeType);
|
|
||||||
if (entry == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto& slot = entry->value;
|
|
||||||
fn = slot.computeFn;
|
|
||||||
userData = slot.userData;
|
|
||||||
modContext = slot.ownerContext;
|
|
||||||
owner = entry->owner;
|
|
||||||
ownerId = slot.ownerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
GfxComputeContext computeContext{
|
|
||||||
.struct_size = sizeof(GfxComputeContext),
|
|
||||||
.device = ctx.device.Get(),
|
|
||||||
.queue = ctx.queue.Get(),
|
|
||||||
.encoder = cmd.Get(),
|
|
||||||
.vertex_buffer = ctx.vertexBuffer.Get(),
|
|
||||||
.index_buffer = ctx.indexBuffer.Get(),
|
|
||||||
.uniform_buffer = ctx.uniformBuffer.Get(),
|
|
||||||
.storage_buffer = ctx.storageBuffer.Get(),
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string failure;
|
|
||||||
try {
|
|
||||||
fn(modContext, &computeContext, payload, payloadSize, userData);
|
|
||||||
return;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
failure = fmt::format("exception in gfx compute callback: {}", e.what());
|
|
||||||
} catch (...) {
|
|
||||||
failure = "unknown exception in gfx compute callback";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
WorkerFailure record{
|
|
||||||
.modId = std::move(ownerId),
|
|
||||||
.message = std::move(failure),
|
|
||||||
};
|
|
||||||
collect_mod_slots_locked(*owner, record.drawIds, record.taskIds);
|
|
||||||
s_workerFailures.push_back(std::move(record));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
ModResult gfx_register_draw_type(
|
|
||||||
LoadedMod& mod, const char* label, GfxDrawFn draw, void* userData, uint64_t& outHandle) {
|
|
||||||
outHandle = 0;
|
|
||||||
|
|
||||||
uint64_t handle = 0;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
handle = s_slots.emplace(mod, GfxSlot{
|
|
||||||
.kind = GfxSlotKind::DrawType,
|
|
||||||
.ownerContext = mod.context.get(),
|
|
||||||
.ownerId = mod.metadata.id,
|
|
||||||
.userData = userData,
|
|
||||||
.drawFn = draw,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto auroraId = aurora::gfx::register_draw_type(aurora::gfx::DrawTypeDescriptor{
|
|
||||||
.label = label,
|
|
||||||
.draw = draw_trampoline,
|
|
||||||
.userdata = reinterpret_cast<void*>(static_cast<uintptr_t>(handle)),
|
|
||||||
});
|
|
||||||
if (auroraId == aurora::gfx::InvalidDrawType) {
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
s_slots.erase_owned(handle, mod);
|
|
||||||
return MOD_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
if (auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType)) {
|
|
||||||
slot->auroraDrawId = auroraId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outHandle = handle;
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_unregister_draw_type(LoadedMod& mod, uint64_t handle) {
|
|
||||||
aurora::gfx::DrawTypeId auroraId = aurora::gfx::InvalidDrawType;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType);
|
|
||||||
if (slot == nullptr) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
auroraId = slot->auroraDrawId;
|
|
||||||
s_slots.erase_owned(handle, mod);
|
|
||||||
}
|
|
||||||
aurora::gfx::unregister_draw_type(auroraId);
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_draw(LoadedMod& mod, uint64_t handle, const void* payload, size_t payloadSize) {
|
|
||||||
aurora::gfx::DrawTypeId auroraId = aurora::gfx::InvalidDrawType;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType);
|
|
||||||
if (slot == nullptr) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
auroraId = slot->auroraDrawId;
|
|
||||||
}
|
|
||||||
if (!aurora::gfx::push_custom_draw(auroraId, payload, payloadSize)) {
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_stream(
|
|
||||||
GfxStreamBuffer buffer, const void* data, size_t size, size_t alignment, GfxRange& outRange) {
|
|
||||||
aurora::gfx::Range range;
|
|
||||||
const auto* bytes = static_cast<const uint8_t*>(data);
|
|
||||||
switch (buffer) {
|
|
||||||
case GfxStreamBuffer::Verts:
|
|
||||||
range = aurora::gfx::push_verts(bytes, size, alignment);
|
|
||||||
break;
|
|
||||||
case GfxStreamBuffer::Indices:
|
|
||||||
range = aurora::gfx::push_indices(bytes, size, alignment);
|
|
||||||
break;
|
|
||||||
case GfxStreamBuffer::Uniform:
|
|
||||||
range = aurora::gfx::push_uniform(bytes, size);
|
|
||||||
break;
|
|
||||||
case GfxStreamBuffer::Storage:
|
|
||||||
range = aurora::gfx::push_storage(bytes, size);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (range.size == 0) {
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
outRange = GfxRange{.offset = range.offset, .size = range.size};
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_register_stage_hook(
|
|
||||||
LoadedMod& mod, GfxStage stage, GfxStageFn callback, void* userData, uint64_t& outHandle) {
|
|
||||||
outHandle = 0;
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
outHandle = s_slots.emplace(mod, GfxSlot{
|
|
||||||
.kind = GfxSlotKind::StageHook,
|
|
||||||
.ownerContext = mod.context.get(),
|
|
||||||
.ownerId = mod.metadata.id,
|
|
||||||
.userData = userData,
|
|
||||||
.stageFn = callback,
|
|
||||||
.stage = stage,
|
|
||||||
});
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_unregister_stage_hook(LoadedMod& mod, uint64_t handle) {
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::StageHook);
|
|
||||||
if (slot == nullptr) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
s_slots.erase_owned(handle, mod);
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_resolve_pass(LoadedMod& mod, const GfxResolveDesc& desc, GfxResolvedTargets& out) {
|
|
||||||
out = GfxResolvedTargets{.struct_size = sizeof(GfxResolvedTargets)};
|
|
||||||
if (aurora::gfx::is_offscreen() && !s_modOffscreenOpen) {
|
|
||||||
Log.error(
|
|
||||||
"[{}] resolve_pass: the active offscreen pass belongs to the game", mod.metadata.id);
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
const bool closesModOffscreen = s_modOffscreenOpen;
|
|
||||||
|
|
||||||
aurora::gfx::ResolvedTargets resolved;
|
|
||||||
if (!aurora::gfx::resolve_pass(
|
|
||||||
aurora::gfx::ResolveDesc{.color = desc.color, .depth = desc.depth}, resolved))
|
|
||||||
{
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
if (closesModOffscreen) {
|
|
||||||
s_modOffscreenOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
out.color = resolved.color.Get();
|
|
||||||
out.depth = resolved.depth.Get();
|
|
||||||
out.color_format = static_cast<WGPUTextureFormat>(resolved.colorFormat);
|
|
||||||
out.width = resolved.width;
|
|
||||||
out.height = resolved.height;
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_create_pass(LoadedMod& mod, uint32_t width, uint32_t height) {
|
|
||||||
if (aurora::gfx::is_offscreen()) {
|
|
||||||
Log.error("[{}] create_pass: an offscreen pass is already active", mod.metadata.id);
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
if (!aurora::gfx::create_pass(width, height)) {
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
s_modOffscreenOpen = true;
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_register_compute_type(
|
|
||||||
LoadedMod& mod, const char* label, GfxComputeFn callback, void* userData, uint64_t& outHandle) {
|
|
||||||
outHandle = 0;
|
|
||||||
|
|
||||||
uint64_t handle = 0;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
handle = s_slots.emplace(mod, GfxSlot{
|
|
||||||
.kind = GfxSlotKind::ComputeType,
|
|
||||||
.ownerContext = mod.context.get(),
|
|
||||||
.ownerId = mod.metadata.id,
|
|
||||||
.userData = userData,
|
|
||||||
.computeFn = callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto auroraId =
|
|
||||||
aurora::gfx::register_encoder_task_type(aurora::gfx::EncoderTaskDescriptor{
|
|
||||||
.label = label,
|
|
||||||
.callback = compute_trampoline,
|
|
||||||
.userdata = reinterpret_cast<void*>(static_cast<uintptr_t>(handle)),
|
|
||||||
});
|
|
||||||
if (auroraId == aurora::gfx::InvalidEncoderTask) {
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
s_slots.erase_owned(handle, mod);
|
|
||||||
return MOD_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
if (auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType)) {
|
|
||||||
slot->auroraTaskId = auroraId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outHandle = handle;
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_unregister_compute_type(LoadedMod& mod, uint64_t handle) {
|
|
||||||
aurora::gfx::EncoderTaskId auroraId = aurora::gfx::InvalidEncoderTask;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType);
|
|
||||||
if (slot == nullptr) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
auroraId = slot->auroraTaskId;
|
|
||||||
s_slots.erase_owned(handle, mod);
|
|
||||||
}
|
|
||||||
aurora::gfx::unregister_encoder_task_type(auroraId);
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_compute(
|
|
||||||
LoadedMod& mod, uint64_t handle, const void* payload, size_t payloadSize) {
|
|
||||||
aurora::gfx::EncoderTaskId auroraId = aurora::gfx::InvalidEncoderTask;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType);
|
|
||||||
if (slot == nullptr) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
auroraId = slot->auroraTaskId;
|
|
||||||
}
|
|
||||||
if (!aurora::gfx::push_encoder_task(auroraId, payload, payloadSize)) {
|
|
||||||
return MOD_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
void gfx_run_stage(
|
|
||||||
GfxStage stage, const view_class* gameView, const view_port_class* gameViewport) {
|
|
||||||
struct StageEntry {
|
|
||||||
uint64_t handle;
|
|
||||||
GfxStageFn fn;
|
|
||||||
void* userData;
|
|
||||||
ModContext* context;
|
|
||||||
LoadedMod* owner;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<StageEntry> entries;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
s_slots.for_each([&](uint64_t handle, const auto& slotEntry) {
|
|
||||||
const auto& slot = slotEntry.value;
|
|
||||||
if (slot.kind == GfxSlotKind::StageHook && slot.stage == stage) {
|
|
||||||
entries.push_back(StageEntry{
|
|
||||||
.handle = handle,
|
|
||||||
.fn = slot.stageFn,
|
|
||||||
.userData = slot.userData,
|
|
||||||
.context = slot.ownerContext,
|
|
||||||
.owner = slotEntry.owner,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (entries.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GfxStageContext stageContext{
|
|
||||||
.struct_size = sizeof(GfxStageContext),
|
|
||||||
.stage = stage,
|
|
||||||
.game_view = gameView,
|
|
||||||
.game_viewport = gameViewport,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& entry : entries) {
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
if (resolve_slot_locked(entry.handle, GfxSlotKind::StageHook) == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!entry.owner->active) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool wasOffscreen = aurora::gfx::is_offscreen();
|
|
||||||
try {
|
|
||||||
entry.fn(entry.context, &stageContext, entry.userData);
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
fail_mod(*entry.owner, MOD_ERROR,
|
|
||||||
fmt::format("exception in gfx stage callback: {}", e.what()));
|
|
||||||
} catch (...) {
|
|
||||||
fail_mod(*entry.owner, MOD_ERROR, "unknown exception in gfx stage callback");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aurora::gfx::is_offscreen() != wasOffscreen) {
|
|
||||||
aurora::gfx::ResolvedTargets discarded;
|
|
||||||
aurora::gfx::resolve_pass(
|
|
||||||
aurora::gfx::ResolveDesc{.color = false, .depth = false}, discarded);
|
|
||||||
s_modOffscreenOpen = false;
|
|
||||||
fail_mod(*entry.owner, MOD_ERROR,
|
|
||||||
"gfx stage callback returned with its offscreen pass still open");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void gfx_drain_worker_failures() {
|
|
||||||
std::vector<WorkerFailure> failures;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
failures.swap(s_workerFailures);
|
|
||||||
}
|
|
||||||
if (failures.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool needsSynchronize = false;
|
|
||||||
for (const auto& failure : failures) {
|
|
||||||
unregister_aurora_types(failure.drawIds, failure.taskIds);
|
|
||||||
needsSynchronize = needsSynchronize || !failure.drawIds.empty() || !failure.taskIds.empty();
|
|
||||||
}
|
|
||||||
if (needsSynchronize) {
|
|
||||||
aurora::gfx::synchronize();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& failure : failures) {
|
|
||||||
for (auto& mod : ModLoader::instance().mods()) {
|
|
||||||
if (mod.metadata.id == failure.modId && mod.active) {
|
|
||||||
fail_mod(mod, MOD_ERROR, failure.message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void gfx_remove_mod(LoadedMod& mod) {
|
|
||||||
std::vector<aurora::gfx::DrawTypeId> drawIds;
|
|
||||||
std::vector<aurora::gfx::EncoderTaskId> taskIds;
|
|
||||||
{
|
|
||||||
std::lock_guard lock{s_mutex};
|
|
||||||
collect_mod_slots_locked(mod, drawIds, taskIds);
|
|
||||||
}
|
|
||||||
if (drawIds.empty() && taskIds.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
unregister_aurora_types(drawIds, taskIds);
|
|
||||||
aurora::gfx::synchronize();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace dusk::mods
|
|
||||||
|
|
||||||
namespace dusk::mods::svc {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
ModResult gfx_get_device_info(ModContext* context, GfxDeviceInfo* outInfo) {
|
|
||||||
if (outInfo == nullptr || outInfo->struct_size < sizeof(GfxDeviceInfo)) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
const uint32_t structSize = outInfo->struct_size;
|
|
||||||
*outInfo = GfxDeviceInfo{.struct_size = structSize};
|
|
||||||
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
outInfo->device = aurora::gfx::device().Get();
|
|
||||||
outInfo->queue = aurora::gfx::queue().Get();
|
|
||||||
outInfo->color_format = static_cast<WGPUTextureFormat>(aurora::gfx::color_format());
|
|
||||||
outInfo->depth_format = static_cast<WGPUTextureFormat>(aurora::gfx::depth_format());
|
|
||||||
outInfo->sample_count = aurora::gfx::sample_count();
|
|
||||||
outInfo->uses_reversed_z = aurora::gfx::uses_reversed_z();
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
void* gfx_get_proc_address(ModContext* context, const char* name) {
|
|
||||||
if (mod_from_context(context) == nullptr || name == nullptr) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return reinterpret_cast<void*>(wgpuGetProcAddress(WGPUStringView{name, WGPU_STRLEN}));
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_register_draw_type_impl(
|
|
||||||
ModContext* context, const GfxDrawTypeDesc* desc, GfxDrawTypeHandle* outHandle) {
|
|
||||||
if (outHandle != nullptr) {
|
|
||||||
*outHandle = 0;
|
|
||||||
}
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxDrawTypeDesc) ||
|
|
||||||
desc->draw == nullptr || outHandle == nullptr)
|
|
||||||
{
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
uint64_t handle = 0;
|
|
||||||
const auto result =
|
|
||||||
gfx_register_draw_type(*mod, desc->label, desc->draw, desc->user_data, handle);
|
|
||||||
if (result != MOD_OK) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
*outHandle = handle;
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_unregister_draw_type_impl(ModContext* context, GfxDrawTypeHandle handle) {
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || handle == 0) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
return gfx_unregister_draw_type(*mod, handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_draw_impl(
|
|
||||||
ModContext* context, GfxDrawTypeHandle handle, const void* payload, size_t payloadSize) {
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE ||
|
|
||||||
(payloadSize > 0 && payload == nullptr))
|
|
||||||
{
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
return gfx_push_draw(*mod, handle, payload, payloadSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_stream_impl(ModContext* context, GfxStreamBuffer buffer, const void* data,
|
|
||||||
size_t size, size_t alignment, GfxRange* outRange) {
|
|
||||||
if (outRange != nullptr) {
|
|
||||||
*outRange = GfxRange{0, 0};
|
|
||||||
}
|
|
||||||
if (mod_from_context(context) == nullptr || data == nullptr || size == 0 || outRange == nullptr)
|
|
||||||
{
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
return gfx_push_stream(buffer, data, size, alignment, *outRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_verts_impl(
|
|
||||||
ModContext* context, const void* data, size_t size, size_t alignment, GfxRange* outRange) {
|
|
||||||
return gfx_push_stream_impl(context, GfxStreamBuffer::Verts, data, size, alignment, outRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_indices_impl(
|
|
||||||
ModContext* context, const void* data, size_t size, size_t alignment, GfxRange* outRange) {
|
|
||||||
return gfx_push_stream_impl(context, GfxStreamBuffer::Indices, data, size, alignment, outRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_uniform_impl(
|
|
||||||
ModContext* context, const void* data, size_t size, GfxRange* outRange) {
|
|
||||||
return gfx_push_stream_impl(context, GfxStreamBuffer::Uniform, data, size, 0, outRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_storage_impl(
|
|
||||||
ModContext* context, const void* data, size_t size, GfxRange* outRange) {
|
|
||||||
return gfx_push_stream_impl(context, GfxStreamBuffer::Storage, data, size, 0, outRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool valid_stage(GfxStage stage) {
|
|
||||||
return stage == GFX_STAGE_SCENE_AFTER_TERRAIN || stage == GFX_STAGE_FRAME_BEFORE_HUD ||
|
|
||||||
stage == GFX_STAGE_FRAME_AFTER_HUD || stage == GFX_STAGE_SCENE_BEGIN ||
|
|
||||||
stage == GFX_STAGE_SCENE_AFTER_OPAQUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_register_stage_hook_impl(ModContext* context, GfxStage stage,
|
|
||||||
const GfxStageHookDesc* desc, GfxStageHookHandle* outHandle) {
|
|
||||||
if (outHandle != nullptr) {
|
|
||||||
*outHandle = 0;
|
|
||||||
}
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxStageHookDesc) ||
|
|
||||||
desc->callback == nullptr || outHandle == nullptr || !valid_stage(stage))
|
|
||||||
{
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
uint64_t handle = 0;
|
|
||||||
const auto result =
|
|
||||||
gfx_register_stage_hook(*mod, stage, desc->callback, desc->user_data, handle);
|
|
||||||
if (result != MOD_OK) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
*outHandle = handle;
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_unregister_stage_hook_impl(ModContext* context, GfxStageHookHandle handle) {
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || handle == 0) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
return gfx_unregister_stage_hook(*mod, handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_resolve_pass_impl(
|
|
||||||
ModContext* context, const GfxResolveDesc* desc, GfxResolvedTargets* outTargets) {
|
|
||||||
if (outTargets != nullptr && outTargets->struct_size >= sizeof(GfxResolvedTargets)) {
|
|
||||||
*outTargets = GfxResolvedTargets{.struct_size = sizeof(GfxResolvedTargets)};
|
|
||||||
}
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxResolveDesc) ||
|
|
||||||
outTargets == nullptr || outTargets->struct_size < sizeof(GfxResolvedTargets) ||
|
|
||||||
(!desc->color && !desc->depth))
|
|
||||||
{
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
return gfx_resolve_pass(*mod, *desc, *outTargets);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_create_pass_impl(ModContext* context, uint32_t width, uint32_t height) {
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || width == 0 || height == 0) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
return gfx_create_pass(*mod, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_register_compute_type_impl(
|
|
||||||
ModContext* context, const GfxComputeTypeDesc* desc, GfxComputeTypeHandle* outHandle) {
|
|
||||||
if (outHandle != nullptr) {
|
|
||||||
*outHandle = 0;
|
|
||||||
}
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxComputeTypeDesc) ||
|
|
||||||
desc->callback == nullptr || outHandle == nullptr)
|
|
||||||
{
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
uint64_t handle = 0;
|
|
||||||
const auto result =
|
|
||||||
gfx_register_compute_type(*mod, desc->label, desc->callback, desc->user_data, handle);
|
|
||||||
if (result != MOD_OK) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
*outHandle = handle;
|
|
||||||
return MOD_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_unregister_compute_type_impl(ModContext* context, GfxComputeTypeHandle handle) {
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || handle == 0) {
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
return gfx_unregister_compute_type(*mod, handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModResult gfx_push_compute_impl(
|
|
||||||
ModContext* context, GfxComputeTypeHandle handle, const void* payload, size_t payloadSize) {
|
|
||||||
auto* mod = mod_from_context(context);
|
|
||||||
if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE ||
|
|
||||||
(payloadSize > 0 && payload == nullptr))
|
|
||||||
{
|
|
||||||
return MOD_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
return gfx_push_compute(*mod, handle, payload, payloadSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr GfxService s_gfxService{
|
|
||||||
.header = SERVICE_HEADER(GfxService, GFX_SERVICE_MAJOR, GFX_SERVICE_MINOR),
|
|
||||||
.get_device_info = gfx_get_device_info,
|
|
||||||
.get_proc_address = gfx_get_proc_address,
|
|
||||||
.register_draw_type = gfx_register_draw_type_impl,
|
|
||||||
.unregister_draw_type = gfx_unregister_draw_type_impl,
|
|
||||||
.push_draw = gfx_push_draw_impl,
|
|
||||||
.register_compute_type = gfx_register_compute_type_impl,
|
|
||||||
.unregister_compute_type = gfx_unregister_compute_type_impl,
|
|
||||||
.push_compute = gfx_push_compute_impl,
|
|
||||||
.push_verts = gfx_push_verts_impl,
|
|
||||||
.push_indices = gfx_push_indices_impl,
|
|
||||||
.push_uniform = gfx_push_uniform_impl,
|
|
||||||
.push_storage = gfx_push_storage_impl,
|
|
||||||
.register_stage_hook = gfx_register_stage_hook_impl,
|
|
||||||
.unregister_stage_hook = gfx_unregister_stage_hook_impl,
|
|
||||||
.resolve_pass = gfx_resolve_pass_impl,
|
|
||||||
.create_pass = gfx_create_pass_impl,
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
constinit const ServiceModule g_gfxModule{
|
|
||||||
.id = GFX_SERVICE_ID,
|
|
||||||
.majorVersion = GFX_SERVICE_MAJOR,
|
|
||||||
.minorVersion = GFX_SERVICE_MINOR,
|
|
||||||
.service = &s_gfxService,
|
|
||||||
.modDetached = gfx_remove_mod,
|
|
||||||
.frameBegin = gfx_drain_worker_failures,
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace dusk::mods::svc
|
|
||||||
+20
-29
@@ -1,5 +1,4 @@
|
|||||||
#include "registry.hpp"
|
#include "registry.hpp"
|
||||||
#include "slot_map.hpp"
|
|
||||||
|
|
||||||
#include "dusk/mods/loader/loader.hpp"
|
#include "dusk/mods/loader/loader.hpp"
|
||||||
#include "dusk/mods/manifest.hpp"
|
#include "dusk/mods/manifest.hpp"
|
||||||
@@ -64,13 +63,14 @@ const char* host_mod_dir(ModContext* context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct LifecycleWatcher {
|
struct LifecycleWatcher {
|
||||||
|
uint64_t handle = 0;
|
||||||
|
LoadedMod* owner = nullptr;
|
||||||
ModLifecycleFn fn = nullptr;
|
ModLifecycleFn fn = nullptr;
|
||||||
void* userData = nullptr;
|
void* userData = nullptr;
|
||||||
uint64_t order = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SlotMap<LifecycleWatcher> s_watchers;
|
std::vector<LifecycleWatcher> s_watchers;
|
||||||
uint64_t s_nextWatchOrder = 0;
|
uint64_t s_nextWatchHandle = 1;
|
||||||
|
|
||||||
ModResult host_watch_mod_lifecycle(
|
ModResult host_watch_mod_lifecycle(
|
||||||
ModContext* context, ModLifecycleFn fn, void* userData, uint64_t* outHandle) {
|
ModContext* context, ModLifecycleFn fn, void* userData, uint64_t* outHandle) {
|
||||||
@@ -78,8 +78,8 @@ ModResult host_watch_mod_lifecycle(
|
|||||||
if (mod == nullptr || fn == nullptr || outHandle == nullptr) {
|
if (mod == nullptr || fn == nullptr || outHandle == nullptr) {
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
const auto handle = s_watchers.emplace(
|
const auto handle = s_nextWatchHandle++;
|
||||||
*mod, LifecycleWatcher{.fn = fn, .userData = userData, .order = s_nextWatchOrder++});
|
s_watchers.push_back({handle, mod, fn, userData});
|
||||||
*outHandle = handle;
|
*outHandle = handle;
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
@@ -89,41 +89,32 @@ ModResult host_unwatch_mod_lifecycle(ModContext* context, const uint64_t handle)
|
|||||||
if (mod == nullptr) {
|
if (mod == nullptr) {
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
return s_watchers.erase_owned(handle, *mod) ? MOD_OK : MOD_INVALID_ARGUMENT;
|
const auto erased = std::erase_if(s_watchers,
|
||||||
|
[&](const LifecycleWatcher& w) { return w.handle == handle && w.owner == mod; });
|
||||||
|
return erased != 0 ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
void host_mod_detached(LoadedMod& mod) {
|
void host_mod_detached(LoadedMod& mod) {
|
||||||
// The subject's own watches go first: a mod is never notified about its own teardown.
|
// The subject's own watches go first: a mod is never notified about its own teardown.
|
||||||
s_watchers.erase_all(mod);
|
std::erase_if(s_watchers, [&](const LifecycleWatcher& w) { return w.owner == &mod; });
|
||||||
|
|
||||||
// Iterate a snapshot in registration order: callbacks may watch/unwatch, and a failing
|
// Iterate a snapshot: callbacks may watch/unwatch, and a failing callback erases the
|
||||||
// callback erases the failing mod's services.
|
// failing mod's services.
|
||||||
struct PendingNotify {
|
const auto snapshot = s_watchers;
|
||||||
uint64_t order;
|
for (const auto& watcher : snapshot) {
|
||||||
uint64_t handle;
|
const bool alive = std::ranges::any_of(
|
||||||
};
|
s_watchers, [&](const LifecycleWatcher& w) { return w.handle == watcher.handle; });
|
||||||
std::vector<PendingNotify> snapshot;
|
if (!alive) {
|
||||||
s_watchers.for_each([&](const uint64_t handle, const auto& entry) {
|
|
||||||
snapshot.push_back({.order = entry.value.order, .handle = handle});
|
|
||||||
});
|
|
||||||
std::ranges::sort(snapshot, {}, &PendingNotify::order);
|
|
||||||
|
|
||||||
for (const auto& pending : snapshot) {
|
|
||||||
const auto* entry = s_watchers.find(pending.handle);
|
|
||||||
if (entry == nullptr) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Do not retain pointers into SlotMap across a callback that may mutate it.
|
|
||||||
auto* owner = entry->owner;
|
|
||||||
const auto watcher = entry->value;
|
|
||||||
try {
|
try {
|
||||||
watcher.fn(owner->context.get(), mod.context.get(), mod.metadata.id.c_str(),
|
watcher.fn(watcher.owner->context.get(), mod.context.get(), mod.metadata.id.c_str(),
|
||||||
MOD_LIFECYCLE_DETACHED, watcher.userData);
|
MOD_LIFECYCLE_DETACHED, watcher.userData);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
fail_mod(*owner, MOD_ERROR,
|
fail_mod(*watcher.owner, MOD_ERROR,
|
||||||
fmt::format("Exception in mod lifecycle callback: {}", e.what()));
|
fmt::format("Exception in mod lifecycle callback: {}", e.what()));
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
fail_mod(*owner, MOD_ERROR, "Unknown exception in mod lifecycle callback");
|
fail_mod(*watcher.owner, MOD_ERROR, "Unknown exception in mod lifecycle callback");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
#include "registry.hpp"
|
#include "registry.hpp"
|
||||||
#include "slot_map.hpp"
|
|
||||||
|
|
||||||
#include "aurora/dvd.h"
|
#include "aurora/dvd.h"
|
||||||
#include "aurora/lib/logging.hpp"
|
#include "aurora/lib/logging.hpp"
|
||||||
#include "dusk/mods/loader/loader.hpp"
|
#include "dusk/mods/loader/loader.hpp"
|
||||||
#include "mods/svc/overlay.h"
|
#include "mods/svc/overlay.h"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -35,15 +33,15 @@ std::unordered_map<uintptr_t, OverlayFileData> s_overlayFiles;
|
|||||||
uintptr_t s_nextOverlayId = 1;
|
uintptr_t s_nextOverlayId = 1;
|
||||||
std::mutex s_overlayMutex;
|
std::mutex s_overlayMutex;
|
||||||
|
|
||||||
struct RuntimeOverlaySlot {
|
struct RuntimeOverlayEntry {
|
||||||
|
uint64_t handle = 0;
|
||||||
std::string discPath;
|
std::string discPath;
|
||||||
std::string bundlePath; // bundle-backed if non-empty
|
std::string bundlePath; // bundle-backed if non-empty
|
||||||
std::shared_ptr<const std::vector<u8>> buffer; // buffer-backed otherwise
|
std::shared_ptr<const std::vector<u8>> buffer; // buffer-backed otherwise
|
||||||
size_t size = 0;
|
size_t size = 0;
|
||||||
uint64_t order = 0;
|
|
||||||
};
|
};
|
||||||
SlotMap<RuntimeOverlaySlot> s_runtimeOverlays;
|
std::unordered_map<const LoadedMod*, std::vector<RuntimeOverlayEntry>> s_runtimeOverlays;
|
||||||
uint64_t s_nextRuntimeOrder = 0;
|
uint64_t s_nextRuntimeHandle = 1;
|
||||||
bool s_overlaysDirty = false;
|
bool s_overlaysDirty = false;
|
||||||
|
|
||||||
// Aurora matches overlay paths against the disc case-insensitively and later entries win, so
|
// Aurora matches overlay paths against the disc case-insensitively and later entries win, so
|
||||||
@@ -86,25 +84,20 @@ void find_overlay_files(std::vector<AuroraOverlayFile>& files, LoadedMod& mod,
|
|||||||
|
|
||||||
void append_runtime_overlays(std::vector<AuroraOverlayFile>& files, LoadedMod& mod,
|
void append_runtime_overlays(std::vector<AuroraOverlayFile>& files, LoadedMod& mod,
|
||||||
std::unordered_map<std::string, const LoadedMod*>& claims) {
|
std::unordered_map<std::string, const LoadedMod*>& claims) {
|
||||||
// Aurora resolves duplicate paths later-entry-wins, so emit in registration order (SlotMap
|
const auto it = s_runtimeOverlays.find(&mod);
|
||||||
// iteration is index order, and freed indices are reused).
|
if (it == s_runtimeOverlays.end()) {
|
||||||
std::vector<const RuntimeOverlaySlot*> slots;
|
return;
|
||||||
s_runtimeOverlays.for_each([&](uint64_t, const auto& entry) {
|
}
|
||||||
if (entry.owner == &mod) {
|
|
||||||
slots.push_back(&entry.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
std::ranges::sort(slots, {}, &RuntimeOverlaySlot::order);
|
|
||||||
|
|
||||||
for (const auto* slot : slots) {
|
for (const auto& entry : it->second) {
|
||||||
const auto id = s_nextOverlayId++;
|
const auto id = s_nextOverlayId++;
|
||||||
if (slot->buffer != nullptr) {
|
if (entry.buffer != nullptr) {
|
||||||
s_overlayFiles.emplace(id, OverlayFileData{{}, nullptr, slot->buffer});
|
s_overlayFiles.emplace(id, OverlayFileData{{}, nullptr, entry.buffer});
|
||||||
} else {
|
} else {
|
||||||
s_overlayFiles.emplace(id, OverlayFileData{slot->bundlePath, mod.bundle, nullptr});
|
s_overlayFiles.emplace(id, OverlayFileData{entry.bundlePath, mod.bundle, nullptr});
|
||||||
}
|
}
|
||||||
claim_overlay_path(claims, slot->discPath, mod);
|
claim_overlay_path(claims, entry.discPath, mod);
|
||||||
files.emplace_back(strdup(slot->discPath.c_str()), reinterpret_cast<void*>(id), slot->size);
|
files.emplace_back(strdup(entry.discPath.c_str()), reinterpret_cast<void*>(id), entry.size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,39 +201,47 @@ void overlay_sync_files() {
|
|||||||
|
|
||||||
uint64_t overlay_add_file(
|
uint64_t overlay_add_file(
|
||||||
LoadedMod& mod, std::string discPath, std::string bundlePath, size_t size) {
|
LoadedMod& mod, std::string discPath, std::string bundlePath, size_t size) {
|
||||||
const auto handle = s_runtimeOverlays.emplace(mod, RuntimeOverlaySlot{
|
const auto handle = s_nextRuntimeHandle++;
|
||||||
.discPath = std::move(discPath),
|
s_runtimeOverlays[&mod].push_back({
|
||||||
.bundlePath = std::move(bundlePath),
|
.handle = handle,
|
||||||
.size = size,
|
.discPath = std::move(discPath),
|
||||||
.order = s_nextRuntimeOrder++,
|
.bundlePath = std::move(bundlePath),
|
||||||
});
|
.size = size,
|
||||||
|
});
|
||||||
s_overlaysDirty = true;
|
s_overlaysDirty = true;
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t overlay_add_buffer(LoadedMod& mod, std::string discPath, std::vector<u8> data) {
|
uint64_t overlay_add_buffer(LoadedMod& mod, std::string discPath, std::vector<u8> data) {
|
||||||
|
const auto handle = s_nextRuntimeHandle++;
|
||||||
const auto size = data.size();
|
const auto size = data.size();
|
||||||
const auto handle = s_runtimeOverlays.emplace(mod,
|
s_runtimeOverlays[&mod].push_back({
|
||||||
RuntimeOverlaySlot{
|
.handle = handle,
|
||||||
.discPath = std::move(discPath),
|
.discPath = std::move(discPath),
|
||||||
.buffer = std::make_shared<const std::vector<u8>>(std::move(data)),
|
.buffer = std::make_shared<const std::vector<u8>>(std::move(data)),
|
||||||
.size = size,
|
.size = size,
|
||||||
.order = s_nextRuntimeOrder++,
|
});
|
||||||
});
|
|
||||||
s_overlaysDirty = true;
|
s_overlaysDirty = true;
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool overlay_remove(LoadedMod& mod, uint64_t handle) {
|
bool overlay_remove(LoadedMod& mod, uint64_t handle) {
|
||||||
if (!s_runtimeOverlays.erase_owned(handle, mod)) {
|
const auto it = s_runtimeOverlays.find(&mod);
|
||||||
|
if (it == s_runtimeOverlays.end()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (std::erase_if(it->second, [&](const auto& entry) { return entry.handle == handle; }) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (it->second.empty()) {
|
||||||
|
s_runtimeOverlays.erase(it);
|
||||||
|
}
|
||||||
s_overlaysDirty = true;
|
s_overlaysDirty = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void overlay_remove_mod(LoadedMod& mod) {
|
void overlay_remove_mod(LoadedMod& mod) {
|
||||||
if (s_runtimeOverlays.erase_all(mod) != 0) {
|
if (s_runtimeOverlays.erase(&mod) != 0) {
|
||||||
s_overlaysDirty = true;
|
s_overlaysDirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,8 +208,6 @@ void ModLoader::init_services() {
|
|||||||
&svc::g_configModule,
|
&svc::g_configModule,
|
||||||
&svc::g_uiModule,
|
&svc::g_uiModule,
|
||||||
&svc::g_gameModule,
|
&svc::g_gameModule,
|
||||||
&svc::g_cameraModule,
|
|
||||||
&svc::g_gfxModule,
|
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
svc::register_module(*module);
|
svc::register_module(*module);
|
||||||
|
|||||||
@@ -70,7 +70,5 @@ extern const ServiceModule g_textureModule;
|
|||||||
extern const ServiceModule g_configModule;
|
extern const ServiceModule g_configModule;
|
||||||
extern const ServiceModule g_uiModule;
|
extern const ServiceModule g_uiModule;
|
||||||
extern const ServiceModule g_gameModule;
|
extern const ServiceModule g_gameModule;
|
||||||
extern const ServiceModule g_cameraModule;
|
|
||||||
extern const ServiceModule g_gfxModule;
|
|
||||||
|
|
||||||
} // namespace dusk::mods::svc
|
} // namespace dusk::mods::svc
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <limits>
|
|
||||||
#include <optional>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <type_traits>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace dusk::mods {
|
|
||||||
|
|
||||||
struct LoadedMod;
|
|
||||||
|
|
||||||
namespace svc {
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
class SlotMap {
|
|
||||||
public:
|
|
||||||
static_assert(std::is_nothrow_move_constructible_v<T>);
|
|
||||||
|
|
||||||
using Handle = uint64_t;
|
|
||||||
static constexpr Handle InvalidHandle = 0;
|
|
||||||
|
|
||||||
struct Entry {
|
|
||||||
LoadedMod* owner = nullptr;
|
|
||||||
T value;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
Handle emplace(LoadedMod& owner, Args&&... args) {
|
|
||||||
T value{std::forward<Args>(args)...};
|
|
||||||
const auto index = allocate_index();
|
|
||||||
auto& slot = m_slots[index];
|
|
||||||
slot.entry.emplace(Entry{.owner = &owner, .value = std::move(value)});
|
|
||||||
return make_handle(index, slot.generation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returned pointers remain valid only until the next mutating operation.
|
|
||||||
Entry* find(Handle handle) {
|
|
||||||
auto* slot = find_slot(handle);
|
|
||||||
return slot != nullptr ? &*slot->entry : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Entry* find(Handle handle) const {
|
|
||||||
const auto* slot = find_slot(handle);
|
|
||||||
return slot != nullptr ? &*slot->entry : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Entry* find_owned(Handle handle, const LoadedMod& owner) {
|
|
||||||
auto* entry = find(handle);
|
|
||||||
return entry != nullptr && entry->owner == &owner ? entry : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Entry* find_owned(Handle handle, const LoadedMod& owner) const {
|
|
||||||
const auto* entry = find(handle);
|
|
||||||
return entry != nullptr && entry->owner == &owner ? entry : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<Entry> take(Handle handle) {
|
|
||||||
const auto index = handle_index(handle);
|
|
||||||
auto* slot = find_slot(handle);
|
|
||||||
if (slot == nullptr) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
std::optional<Entry> entry{std::move(slot->entry)};
|
|
||||||
release_slot(index);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<Entry> take_owned(Handle handle, const LoadedMod& owner) {
|
|
||||||
if (find_owned(handle, owner) == nullptr) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return take(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Entry> take_all(const LoadedMod& owner) {
|
|
||||||
std::vector<Entry> entries;
|
|
||||||
for (size_t slotIndex = 0; slotIndex < m_slots.size(); ++slotIndex) {
|
|
||||||
const auto index = static_cast<uint32_t>(slotIndex);
|
|
||||||
auto& slot = m_slots[index];
|
|
||||||
if (!slot.entry.has_value() || slot.entry->owner != &owner) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entries.push_back(std::move(*slot.entry));
|
|
||||||
release_slot(index);
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool erase(Handle handle) {
|
|
||||||
const auto index = handle_index(handle);
|
|
||||||
if (find_slot(handle) == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
release_slot(index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool erase_owned(Handle handle, const LoadedMod& owner) {
|
|
||||||
if (find_owned(handle, owner) == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return erase(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t erase_all(const LoadedMod& owner) {
|
|
||||||
return take_all(owner).size();
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Fn>
|
|
||||||
void for_each(Fn&& fn) const {
|
|
||||||
// The visitor may inspect entries but must not mutate this SlotMap.
|
|
||||||
for (size_t slotIndex = 0; slotIndex < m_slots.size(); ++slotIndex) {
|
|
||||||
const auto index = static_cast<uint32_t>(slotIndex);
|
|
||||||
const auto& slot = m_slots[index];
|
|
||||||
if (slot.entry.has_value()) {
|
|
||||||
fn(make_handle(index, slot.generation), *slot.entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct Slot {
|
|
||||||
uint32_t generation = 1;
|
|
||||||
std::optional<Entry> entry;
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr Handle make_handle(uint32_t index, uint32_t generation) {
|
|
||||||
return static_cast<Handle>(generation) << 32 | index;
|
|
||||||
}
|
|
||||||
|
|
||||||
static constexpr uint32_t handle_index(Handle handle) {
|
|
||||||
return static_cast<uint32_t>(handle & std::numeric_limits<uint32_t>::max());
|
|
||||||
}
|
|
||||||
|
|
||||||
static constexpr uint32_t handle_generation(Handle handle) {
|
|
||||||
return static_cast<uint32_t>(handle >> 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
Slot* find_slot(Handle handle) {
|
|
||||||
return const_cast<Slot*>(std::as_const(*this).find_slot(handle));
|
|
||||||
}
|
|
||||||
|
|
||||||
const Slot* find_slot(Handle handle) const {
|
|
||||||
const auto index = handle_index(handle);
|
|
||||||
if (handle == InvalidHandle || index >= m_slots.size()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
const auto& slot = m_slots[index];
|
|
||||||
if (!slot.entry.has_value() || slot.generation != handle_generation(handle)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return &slot;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t allocate_index() {
|
|
||||||
if (!m_freeSlots.empty()) {
|
|
||||||
const auto index = m_freeSlots.back();
|
|
||||||
m_freeSlots.pop_back();
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
if (m_slots.size() > std::numeric_limits<uint32_t>::max()) {
|
|
||||||
throw std::length_error{"SlotMap handle space exhausted"};
|
|
||||||
}
|
|
||||||
const auto index = static_cast<uint32_t>(m_slots.size());
|
|
||||||
m_slots.emplace_back();
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
void release_slot(uint32_t index) {
|
|
||||||
auto& slot = m_slots[index];
|
|
||||||
slot.entry.reset();
|
|
||||||
if (slot.generation == std::numeric_limits<uint32_t>::max()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
++slot.generation;
|
|
||||||
m_freeSlots.push_back(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Slot> m_slots;
|
|
||||||
std::vector<uint32_t> m_freeSlots;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace svc
|
|
||||||
} // namespace dusk::mods
|
|
||||||
+103
-41
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
#include "registry.hpp"
|
#include "registry.hpp"
|
||||||
#include "slot_map.hpp"
|
|
||||||
|
|
||||||
#include "aurora/lib/logging.hpp"
|
#include "aurora/lib/logging.hpp"
|
||||||
#include "dusk/mod_loader.hpp"
|
#include "dusk/mod_loader.hpp"
|
||||||
@@ -34,6 +33,7 @@ namespace {
|
|||||||
aurora::Module Log("dusk::mods::ui");
|
aurora::Module Log("dusk::mods::ui");
|
||||||
|
|
||||||
enum class UiSlotKind : u8 {
|
enum class UiSlotKind : u8 {
|
||||||
|
Free,
|
||||||
Window,
|
Window,
|
||||||
Dialog,
|
Dialog,
|
||||||
Pane,
|
Pane,
|
||||||
@@ -63,14 +63,17 @@ const char* slot_kind_name(UiSlotKind kind) {
|
|||||||
case UiSlotKind::MenuTab:
|
case UiSlotKind::MenuTab:
|
||||||
return "menu tab";
|
return "menu tab";
|
||||||
default:
|
default:
|
||||||
return "unknown";
|
return "free";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Game thread only: all mutations happen in service calls made from mod code, in UI callbacks
|
// Generation-checked handle slots. Game thread only: all mutations happen in service calls made
|
||||||
// (ui::update), or in the loader's deactivate paths.
|
// from mod code, in UI callbacks (ui::update), or in the loader's deactivate paths.
|
||||||
struct UiSlot {
|
struct UiSlot {
|
||||||
UiSlotKind kind = UiSlotKind::Window;
|
// Bumped on free, which invalidates every outstanding handle to this slot.
|
||||||
|
uint32_t generation = 1;
|
||||||
|
UiSlotKind kind = UiSlotKind::Free;
|
||||||
|
LoadedMod* owner = nullptr;
|
||||||
// Pane/Text/Progress/Control: freed automatically when the element is destroyed
|
// Pane/Text/Progress/Control: freed automatically when the element is destroyed
|
||||||
Rml::Element* element = nullptr;
|
Rml::Element* element = nullptr;
|
||||||
// Pane payload
|
// Pane payload
|
||||||
@@ -90,7 +93,8 @@ struct UiSlot {
|
|||||||
bool hasElementValue = false;
|
bool hasElementValue = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
SlotMap<UiSlot> s_slots;
|
std::vector<UiSlot> s_slots;
|
||||||
|
std::vector<uint32_t> s_freeSlots;
|
||||||
|
|
||||||
struct ModUiPanel {
|
struct ModUiPanel {
|
||||||
UiPanelBuildFn build = nullptr;
|
UiPanelBuildFn build = nullptr;
|
||||||
@@ -108,26 +112,71 @@ struct ModMenuTab {
|
|||||||
std::unordered_map<const LoadedMod*, std::vector<ModMenuTab>> s_modMenuTabs;
|
std::unordered_map<const LoadedMod*, std::vector<ModMenuTab>> s_modMenuTabs;
|
||||||
bool s_menuTabsDirty = false;
|
bool s_menuTabsDirty = false;
|
||||||
|
|
||||||
|
uint64_t handle_for(uint32_t index) {
|
||||||
|
return (uint64_t{s_slots[index].generation} << 32) | index;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t slot_index(const UiSlot& slot) {
|
||||||
|
return static_cast<uint32_t>(&slot - s_slots.data());
|
||||||
|
}
|
||||||
|
|
||||||
UiSlot* slot_from_handle(uint64_t handle) {
|
UiSlot* slot_from_handle(uint64_t handle) {
|
||||||
auto* entry = s_slots.find(handle);
|
const auto index = static_cast<uint32_t>(handle & 0xFFFFFFFFu);
|
||||||
return entry != nullptr ? &entry->value : nullptr;
|
const auto generation = static_cast<uint32_t>(handle >> 32);
|
||||||
|
if (index >= s_slots.size()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto& slot = s_slots[index];
|
||||||
|
if (slot.kind == UiSlotKind::Free || slot.generation != generation) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: s_slots may reallocate on any later allocation, so callers must not hold the returned
|
// Note: s_slots may reallocate on any later allocation, so callers must not hold the returned
|
||||||
// slot reference across calls that can allocate (e.g. mod build callbacks); re-resolve instead.
|
// slot reference across calls that can allocate (e.g. mod build callbacks); re-resolve instead.
|
||||||
UiSlot& alloc_slot(LoadedMod& mod, UiSlotKind kind, uint64_t& outHandle) {
|
UiSlot& alloc_slot(LoadedMod& mod, UiSlotKind kind, uint64_t& outHandle) {
|
||||||
outHandle = s_slots.emplace(mod, UiSlot{.kind = kind});
|
uint32_t index;
|
||||||
return s_slots.find(outHandle)->value;
|
if (!s_freeSlots.empty()) {
|
||||||
|
index = s_freeSlots.back();
|
||||||
|
s_freeSlots.pop_back();
|
||||||
|
} else {
|
||||||
|
index = static_cast<uint32_t>(s_slots.size());
|
||||||
|
s_slots.emplace_back();
|
||||||
|
}
|
||||||
|
auto& slot = s_slots[index];
|
||||||
|
slot.kind = kind;
|
||||||
|
slot.owner = &mod;
|
||||||
|
outHandle = handle_for(index);
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
void free_slot(UiSlot& slot) {
|
||||||
|
slot.generation++;
|
||||||
|
slot.kind = UiSlotKind::Free;
|
||||||
|
slot.owner = nullptr;
|
||||||
|
slot.element = nullptr;
|
||||||
|
slot.pane = nullptr;
|
||||||
|
slot.helpPane = nullptr;
|
||||||
|
slot.document = nullptr;
|
||||||
|
slot.onClosed = nullptr;
|
||||||
|
slot.onClosedUserData = nullptr;
|
||||||
|
slot.styleScope = ui::DocumentScope::None;
|
||||||
|
slot.styleId.clear();
|
||||||
|
slot.elementRml.clear();
|
||||||
|
slot.elementFloat = 0.0f;
|
||||||
|
slot.hasElementValue = false;
|
||||||
|
s_freeSlots.push_back(slot_index(slot));
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSlot* resolve(LoadedMod& mod, uint64_t handle, UiSlotKind kind, const char* what) {
|
UiSlot* resolve(LoadedMod& mod, uint64_t handle, UiSlotKind kind, const char* what) {
|
||||||
auto* entry = s_slots.find_owned(handle, mod);
|
auto* slot = slot_from_handle(handle);
|
||||||
if (entry == nullptr || entry->value.kind != kind) {
|
if (slot == nullptr || slot->owner != &mod || slot->kind != kind) {
|
||||||
Log.error("[{}] {}: stale or invalid {} handle {:#x}", mod.metadata.id, what,
|
Log.error("[{}] {}: stale or invalid {} handle {:#x}", mod.metadata.id, what,
|
||||||
slot_kind_name(kind), handle);
|
slot_kind_name(kind), handle);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return &entry->value;
|
return slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether the registration a callback was created under is still live. Callbacks captured by
|
// Whether the registration a callback was created under is still live. Callbacks captured by
|
||||||
@@ -135,7 +184,7 @@ UiSlot* resolve(LoadedMod& mod, uint64_t handle, UiSlotKind kind, const char* wh
|
|||||||
// once a reload completes, but captured fn pointers still target the unloaded image. Teardown
|
// once a reload completes, but captured fn pointers still target the unloaded image. Teardown
|
||||||
// frees the slots (ui_remove_mod), which invalidates every callback built under them.
|
// frees the slots (ui_remove_mod), which invalidates every callback built under them.
|
||||||
bool slot_live(uint64_t handle) {
|
bool slot_live(uint64_t handle) {
|
||||||
return s_slots.find(handle) != nullptr;
|
return slot_from_handle(handle) != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool dialog_open(uint64_t handle) {
|
bool dialog_open(uint64_t handle) {
|
||||||
@@ -148,22 +197,30 @@ bool dialog_open(uint64_t handle) {
|
|||||||
// The generation check makes a late detach of an already-recycled slot a no-op.
|
// The generation check makes a late detach of an already-recycled slot a no-op.
|
||||||
class SlotDetachListener final : public Rml::EventListener {
|
class SlotDetachListener final : public Rml::EventListener {
|
||||||
public:
|
public:
|
||||||
explicit SlotDetachListener(uint64_t handle) : m_handle{handle} {}
|
SlotDetachListener(uint32_t index, uint32_t generation)
|
||||||
|
: m_index{index}, m_generation{generation} {}
|
||||||
|
|
||||||
void ProcessEvent(Rml::Event&) override {}
|
void ProcessEvent(Rml::Event&) override {}
|
||||||
|
|
||||||
void OnDetach(Rml::Element*) override {
|
void OnDetach(Rml::Element*) override {
|
||||||
s_slots.erase(m_handle);
|
if (m_index < s_slots.size()) {
|
||||||
|
auto& slot = s_slots[m_index];
|
||||||
|
if (slot.kind != UiSlotKind::Free && slot.generation == m_generation) {
|
||||||
|
free_slot(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
delete this;
|
delete this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint64_t m_handle;
|
uint32_t m_index;
|
||||||
|
uint32_t m_generation;
|
||||||
};
|
};
|
||||||
|
|
||||||
void track_element(uint64_t handle, UiSlot& slot, Rml::Element& element) {
|
void track_element(UiSlot& slot, Rml::Element& element) {
|
||||||
slot.element = &element;
|
slot.element = &element;
|
||||||
element.AddEventListener(Rml::EventId::Click, new SlotDetachListener{handle});
|
element.AddEventListener(
|
||||||
|
Rml::EventId::Click, new SlotDetachListener(slot_index(slot), slot.generation));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T, typename Fn>
|
template <typename T, typename Fn>
|
||||||
@@ -212,7 +269,7 @@ uint64_t wrap_pane(LoadedMod& mod, ui::Pane& pane, ui::Pane* helpPane) {
|
|||||||
auto& slot = alloc_slot(mod, UiSlotKind::Pane, handle);
|
auto& slot = alloc_slot(mod, UiSlotKind::Pane, handle);
|
||||||
slot.pane = &pane;
|
slot.pane = &pane;
|
||||||
slot.helpPane = helpPane;
|
slot.helpPane = helpPane;
|
||||||
track_element(handle, slot, *pane.root());
|
track_element(slot, *pane.root());
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,14 +446,14 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC
|
|||||||
}
|
}
|
||||||
|
|
||||||
void on_mod_window_destroyed(uint64_t handle) {
|
void on_mod_window_destroyed(uint64_t handle) {
|
||||||
const auto* entry = s_slots.find(handle);
|
auto* slot = slot_from_handle(handle);
|
||||||
if (entry == nullptr || entry->value.kind != UiSlotKind::Window) {
|
if (slot == nullptr || slot->kind != UiSlotKind::Window) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto released = s_slots.take(handle);
|
LoadedMod* mod = slot->owner;
|
||||||
auto* mod = released->owner;
|
const UiWindowClosedFn onClosed = slot->onClosed;
|
||||||
const UiWindowClosedFn onClosed = released->value.onClosed;
|
void* userData = slot->onClosedUserData;
|
||||||
void* userData = released->value.onClosedUserData;
|
free_slot(*slot);
|
||||||
if (mod != nullptr && onClosed != nullptr) {
|
if (mod != nullptr && onClosed != nullptr) {
|
||||||
guarded_call(*mod, "window on_closed callback", [&] {
|
guarded_call(*mod, "window on_closed callback", [&] {
|
||||||
onClosed(mod->context.get(), handle, userData);
|
onClosed(mod->context.get(), handle, userData);
|
||||||
@@ -407,7 +464,7 @@ void on_mod_window_destroyed(uint64_t handle) {
|
|||||||
void on_mod_dialog_destroyed(uint64_t handle) {
|
void on_mod_dialog_destroyed(uint64_t handle) {
|
||||||
auto* slot = slot_from_handle(handle);
|
auto* slot = slot_from_handle(handle);
|
||||||
if (slot != nullptr && slot->kind == UiSlotKind::Dialog) {
|
if (slot != nullptr && slot->kind == UiSlotKind::Dialog) {
|
||||||
s_slots.erase(handle);
|
free_slot(*slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +573,7 @@ ModResult ui_pane_add_text(LoadedMod& mod, uint64_t pane, const char* text, uint
|
|||||||
auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem);
|
auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem);
|
||||||
elemSlot.elementRml = ui::escape(text);
|
elemSlot.elementRml = ui::escape(text);
|
||||||
elemSlot.hasElementValue = true;
|
elemSlot.hasElementValue = true;
|
||||||
track_element(*outElem, elemSlot, *elem);
|
track_element(elemSlot, *elem);
|
||||||
}
|
}
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
@@ -531,7 +588,7 @@ ModResult ui_pane_add_rml(LoadedMod& mod, uint64_t pane, const char* rml, uint64
|
|||||||
auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem);
|
auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem);
|
||||||
elemSlot.elementRml = rml;
|
elemSlot.elementRml = rml;
|
||||||
elemSlot.hasElementValue = true;
|
elemSlot.hasElementValue = true;
|
||||||
track_element(*outElem, elemSlot, *elem);
|
track_element(elemSlot, *elem);
|
||||||
}
|
}
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
@@ -547,7 +604,7 @@ ModResult ui_pane_add_progress(LoadedMod& mod, uint64_t pane, float value, uint6
|
|||||||
auto& elemSlot = alloc_slot(mod, UiSlotKind::Progress, *outElem);
|
auto& elemSlot = alloc_slot(mod, UiSlotKind::Progress, *outElem);
|
||||||
elemSlot.elementFloat = value;
|
elemSlot.elementFloat = value;
|
||||||
elemSlot.hasElementValue = true;
|
elemSlot.hasElementValue = true;
|
||||||
track_element(*outElem, elemSlot, *elem);
|
track_element(elemSlot, *elem);
|
||||||
}
|
}
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
@@ -634,7 +691,7 @@ ModResult ui_pane_add_control(
|
|||||||
}
|
}
|
||||||
if (outElem != nullptr) {
|
if (outElem != nullptr) {
|
||||||
auto& elemSlot = alloc_slot(mod, UiSlotKind::Control, *outElem);
|
auto& elemSlot = alloc_slot(mod, UiSlotKind::Control, *outElem);
|
||||||
track_element(*outElem, elemSlot, *control->root());
|
track_element(elemSlot, *control->root());
|
||||||
}
|
}
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
@@ -683,13 +740,13 @@ ModResult ui_elem_set_progress(LoadedMod& mod, uint64_t elem, float value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ModResult ui_elem_set_class(LoadedMod& mod, uint64_t elem, const char* name, bool active) {
|
ModResult ui_elem_set_class(LoadedMod& mod, uint64_t elem, const char* name, bool active) {
|
||||||
auto* entry = s_slots.find_owned(elem, mod);
|
auto* slot = slot_from_handle(elem);
|
||||||
if (entry == nullptr || entry->value.element == nullptr) {
|
if (slot == nullptr || slot->owner != &mod || slot->element == nullptr) {
|
||||||
Log.error(
|
Log.error(
|
||||||
"[{}] elem_set_class: stale or invalid element handle {:#x}", mod.metadata.id, elem);
|
"[{}] elem_set_class: stale or invalid element handle {:#x}", mod.metadata.id, elem);
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
entry->value.element->SetClass(name, active);
|
slot->element->SetClass(name, active);
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,7 +945,7 @@ ModResult ui_unregister_menu_tab(LoadedMod& mod, uint64_t handle) {
|
|||||||
s_modMenuTabs.erase(it);
|
s_modMenuTabs.erase(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s_slots.erase_owned(handle, mod);
|
free_slot(*slot);
|
||||||
s_menuTabsDirty = true;
|
s_menuTabsDirty = true;
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
@@ -969,7 +1026,7 @@ ModResult ui_register_styles(
|
|||||||
slot.styleId = fmt::format("{}:{:x}", mod.metadata.id, handle);
|
slot.styleId = fmt::format("{}:{:x}", mod.metadata.id, handle);
|
||||||
if (!ui::register_scoped_styles(docScope, slot.styleId, rcss)) {
|
if (!ui::register_scoped_styles(docScope, slot.styleId, rcss)) {
|
||||||
Log.error("[{}] register_styles: failed to parse RCSS", mod.metadata.id);
|
Log.error("[{}] register_styles: failed to parse RCSS", mod.metadata.id);
|
||||||
s_slots.erase(handle);
|
free_slot(slot);
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
outHandle = handle;
|
outHandle = handle;
|
||||||
@@ -999,8 +1056,8 @@ ModResult ui_unregister_styles(LoadedMod& mod, uint64_t handle) {
|
|||||||
if (slot == nullptr) {
|
if (slot == nullptr) {
|
||||||
return MOD_INVALID_ARGUMENT;
|
return MOD_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
auto released = s_slots.take_owned(handle, mod);
|
ui::unregister_scoped_styles(slot->styleScope, slot->styleId);
|
||||||
ui::unregister_scoped_styles(released->value.styleScope, released->value.styleId);
|
free_slot(*slot);
|
||||||
return MOD_OK;
|
return MOD_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,12 +1066,14 @@ void ui_remove_mod(LoadedMod& mod) {
|
|||||||
if (s_modMenuTabs.erase(&mod) != 0) {
|
if (s_modMenuTabs.erase(&mod) != 0) {
|
||||||
s_menuTabsDirty = true;
|
s_menuTabsDirty = true;
|
||||||
}
|
}
|
||||||
auto entries = s_slots.take_all(mod);
|
for (auto& slot : s_slots) {
|
||||||
for (auto& entry : entries) {
|
if (slot.kind == UiSlotKind::Free || slot.owner != &mod) {
|
||||||
auto& slot = entry.value;
|
continue;
|
||||||
|
}
|
||||||
switch (slot.kind) {
|
switch (slot.kind) {
|
||||||
case UiSlotKind::Window: {
|
case UiSlotKind::Window: {
|
||||||
auto* window = static_cast<ui::ModWindow*>(slot.document);
|
auto* window = static_cast<ui::ModWindow*>(slot.document);
|
||||||
|
free_slot(slot);
|
||||||
if (window != nullptr) {
|
if (window != nullptr) {
|
||||||
window->force_close();
|
window->force_close();
|
||||||
}
|
}
|
||||||
@@ -1022,6 +1081,7 @@ void ui_remove_mod(LoadedMod& mod) {
|
|||||||
}
|
}
|
||||||
case UiSlotKind::Dialog: {
|
case UiSlotKind::Dialog: {
|
||||||
auto* dialog = static_cast<ModDialog*>(slot.document);
|
auto* dialog = static_cast<ModDialog*>(slot.document);
|
||||||
|
free_slot(slot);
|
||||||
if (dialog != nullptr) {
|
if (dialog != nullptr) {
|
||||||
dialog->force_close();
|
dialog->force_close();
|
||||||
}
|
}
|
||||||
@@ -1029,8 +1089,10 @@ void ui_remove_mod(LoadedMod& mod) {
|
|||||||
}
|
}
|
||||||
case UiSlotKind::Style:
|
case UiSlotKind::Style:
|
||||||
ui::unregister_scoped_styles(slot.styleScope, slot.styleId);
|
ui::unregister_scoped_styles(slot.styleScope, slot.styleId);
|
||||||
|
free_slot(slot);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
free_slot(slot);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ UserSettings g_userSettings = {
|
|||||||
.fastSpinner {"game.fastSpinner", false},
|
.fastSpinner {"game.fastSpinner", false},
|
||||||
.armorRupeeDrain {"game.armorRupeeDrain", MagicArmorMode::NORMAL},
|
.armorRupeeDrain {"game.armorRupeeDrain", MagicArmorMode::NORMAL},
|
||||||
.invincibleEnemies {"game.invincibleEnemies", false},
|
.invincibleEnemies {"game.invincibleEnemies", false},
|
||||||
|
.easyQuickSpin {"game.easyQuickSpin", false},
|
||||||
|
|
||||||
// Technical
|
// Technical
|
||||||
.restoreWiiGlitches {"game.restoreWiiGlitches", false},
|
.restoreWiiGlitches {"game.restoreWiiGlitches", false},
|
||||||
@@ -315,6 +316,8 @@ void registerSettings() {
|
|||||||
Register(g_userSettings.game.superClawshot);
|
Register(g_userSettings.game.superClawshot);
|
||||||
Register(g_userSettings.game.alwaysGreatspin);
|
Register(g_userSettings.game.alwaysGreatspin);
|
||||||
Register(g_userSettings.game.invincibleEnemies);
|
Register(g_userSettings.game.invincibleEnemies);
|
||||||
|
Register(g_userSettings.game.easyQuickSpin);
|
||||||
|
|
||||||
Register(g_userSettings.game.enableFrameInterpolation);
|
Register(g_userSettings.game.enableFrameInterpolation);
|
||||||
Register(g_userSettings.game.enableGyroAim);
|
Register(g_userSettings.game.enableGyroAim);
|
||||||
Register(g_userSettings.game.enableGyroRollgoal);
|
Register(g_userSettings.game.enableGyroRollgoal);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "dusk/speedrun.h"
|
#include "dusk/speedrun.h"
|
||||||
#include "dusk/settings.h"
|
#include "dusk/settings.h"
|
||||||
#include "dusk/config.hpp"
|
|
||||||
#include "m_Do/m_Do_main.h"
|
#include "m_Do/m_Do_main.h"
|
||||||
#include <aurora/aurora.h>
|
#include <aurora/aurora.h>
|
||||||
|
|
||||||
@@ -44,15 +43,4 @@ void resetForSpeedrunMode() {
|
|||||||
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clearSpeedrunOverrides() {
|
|
||||||
config::EnumerateRegistered([](config::ConfigVarBase& cvar) {
|
|
||||||
cvar.clearSpeedrunOverride();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void restoreFromSpeedrunMode() {
|
|
||||||
clearSpeedrunOverrides();
|
|
||||||
aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace dusk
|
} // namespace dusk
|
||||||
|
|||||||
+16
-11
@@ -72,11 +72,16 @@ std::string format_time(int64_t timeMs) {
|
|||||||
return fmt::format("{}.{:03}", buffer.data(), timeMs % 1000);
|
return fmt::format("{}.{:03}", buffer.data(), timeMs % 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
Rml::Element* append_span(Rml::Element* parent, const char* className, const Rml::String& text) {
|
void append_text(Rml::ElementDocument* doc, Rml::Element* parent, const Rml::String& text) {
|
||||||
auto* span = append(parent, "span");
|
parent->AppendChild(doc->CreateTextNode(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
Rml::Element* append_span(Rml::ElementDocument* doc, Rml::Element* parent, const char* className,
|
||||||
|
const Rml::String& text) {
|
||||||
|
auto span = doc->CreateElement("span");
|
||||||
span->SetClass(className, true);
|
span->SetClass(className, true);
|
||||||
append_text(span, text);
|
append_text(doc, span.get(), text);
|
||||||
return span;
|
return parent->AppendChild(std::move(span));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -260,18 +265,18 @@ Rml::Element* LogsWindow::append_log_line(const mods::log::Line& line) {
|
|||||||
modId = "?";
|
modId = "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* elem = append(mLinesElem, "div");
|
auto elem = mDocument->CreateElement("div");
|
||||||
elem->SetClass("log-line", true);
|
elem->SetClass("log-line", true);
|
||||||
elem->SetClass(level_class(line.level), true);
|
elem->SetClass(level_class(line.level), true);
|
||||||
|
|
||||||
constexpr const char* kNbsp = "\xc2\xa0";
|
constexpr const char* kNbsp = "\xc2\xa0";
|
||||||
append_span(elem, "log-time", format_time(line.timeMs));
|
append_span(mDocument, elem.get(), "log-time", format_time(line.timeMs));
|
||||||
append_text(elem, kNbsp);
|
append_text(mDocument, elem.get(), kNbsp);
|
||||||
append_span(elem, "log-mod", fmt::format("[{}]", modId));
|
append_span(mDocument, elem.get(), "log-mod", fmt::format("[{}]", modId));
|
||||||
append_text(elem, kNbsp);
|
append_text(mDocument, elem.get(), kNbsp);
|
||||||
append_span(elem, "log-msg", line.message);
|
append_span(mDocument, elem.get(), "log-msg", line.message);
|
||||||
|
|
||||||
return elem;
|
return mLinesElem->AppendChild(std::move(elem));
|
||||||
}
|
}
|
||||||
|
|
||||||
void LogsWindow::copy_to_clipboard() {
|
void LogsWindow::copy_to_clipboard() {
|
||||||
|
|||||||
@@ -166,13 +166,13 @@ bool Pane::focus() {
|
|||||||
Rml::Element* Pane::add_section(const Rml::String& text) {
|
Rml::Element* Pane::add_section(const Rml::String& text) {
|
||||||
auto* elem = append(mRoot, "div");
|
auto* elem = append(mRoot, "div");
|
||||||
elem->SetClass("section-heading", true);
|
elem->SetClass("section-heading", true);
|
||||||
append_text(elem, text);
|
elem->SetInnerRML(escape(text));
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rml::Element* Pane::add_text(const Rml::String& text) {
|
Rml::Element* Pane::add_text(const Rml::String& text) {
|
||||||
auto* elem = append(mRoot, "div");
|
auto* elem = append(mRoot, "div");
|
||||||
append_text(elem, text);
|
elem->SetInnerRML(escape(text));
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
#include "dusk/io.hpp"
|
#include "dusk/io.hpp"
|
||||||
#include "dusk/livesplit.h"
|
#include "dusk/livesplit.h"
|
||||||
#include "dusk/discord_presence.hpp"
|
#include "dusk/discord_presence.hpp"
|
||||||
#include "dusk/speedrun.h"
|
|
||||||
#include "graphics_tuner.hpp"
|
#include "graphics_tuner.hpp"
|
||||||
#include "m_Do/m_Do_main.h"
|
#include "m_Do/m_Do_main.h"
|
||||||
#include "menu_bar.hpp"
|
#include "menu_bar.hpp"
|
||||||
@@ -219,6 +218,52 @@ AuroraBackend configured_backend() {
|
|||||||
return configuredBackend;
|
return configuredBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void reset_for_speedrun_mode() {
|
||||||
|
mDoMain::developmentMode = -1;
|
||||||
|
|
||||||
|
getSettings().game.enableTurboKeybind.setSpeedrunValue(false);
|
||||||
|
|
||||||
|
getSettings().game.damageMultiplier.setSpeedrunValue(1);
|
||||||
|
getSettings().game.instantDeath.setSpeedrunValue(false);
|
||||||
|
getSettings().game.noHeartDrops.setSpeedrunValue(false);
|
||||||
|
getSettings().game.autoSave.setSpeedrunValue(false);
|
||||||
|
getSettings().game.sunsSong.setSpeedrunValue(false);
|
||||||
|
|
||||||
|
getSettings().game.infiniteHearts.setSpeedrunValue(false);
|
||||||
|
getSettings().game.infiniteArrows.setSpeedrunValue(false);
|
||||||
|
getSettings().game.infiniteSeeds.setSpeedrunValue(false);
|
||||||
|
getSettings().game.infiniteBombs.setSpeedrunValue(false);
|
||||||
|
getSettings().game.infiniteOil.setSpeedrunValue(false);
|
||||||
|
getSettings().game.infiniteOxygen.setSpeedrunValue(false);
|
||||||
|
getSettings().game.infiniteRupees.setSpeedrunValue(false);
|
||||||
|
getSettings().game.enableIndefiniteItemDrops.setSpeedrunValue(false);
|
||||||
|
getSettings().game.moonJump.setSpeedrunValue(false);
|
||||||
|
getSettings().game.superClawshot.setSpeedrunValue(false);
|
||||||
|
getSettings().game.alwaysGreatspin.setSpeedrunValue(false);
|
||||||
|
getSettings().game.enableFastIronBoots.setSpeedrunValue(false);
|
||||||
|
getSettings().game.canTransformAnywhere.setSpeedrunValue(false);
|
||||||
|
getSettings().game.fastRoll.setSpeedrunValue(false);
|
||||||
|
getSettings().game.fastSpinner.setSpeedrunValue(false);
|
||||||
|
getSettings().game.armorRupeeDrain.setSpeedrunValue(MagicArmorMode::NORMAL);
|
||||||
|
getSettings().game.invincibleEnemies.setSpeedrunValue(false);
|
||||||
|
|
||||||
|
getSettings().game.pauseOnFocusLost.setSpeedrunValue(false);
|
||||||
|
|
||||||
|
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
|
||||||
|
getSettings().game.recordingMode.setSpeedrunValue(false);
|
||||||
|
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_speedrun_overrides() {
|
||||||
|
config::EnumerateRegistered([](config::ConfigVarBase& cvar) {
|
||||||
|
cvar.clearSpeedrunOverride();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void restore_from_speedrun_mode() {
|
||||||
|
clear_speedrun_overrides();
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path normalized_display_path(const std::filesystem::path& path) {
|
std::filesystem::path normalized_display_path(const std::filesystem::path& path) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
auto normalized = std::filesystem::weakly_canonical(path, ec);
|
auto normalized = std::filesystem::weakly_canonical(path, ec);
|
||||||
@@ -1273,9 +1318,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
|||||||
.onChange =
|
.onChange =
|
||||||
[](bool enabled) {
|
[](bool enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
resetForSpeedrunMode();
|
reset_for_speedrun_mode();
|
||||||
} else {
|
} else {
|
||||||
restoreFromSpeedrunMode();
|
restore_from_speedrun_mode();
|
||||||
if (getSettings().game.liveSplitEnabled) {
|
if (getSettings().game.liveSplitEnabled) {
|
||||||
speedrun::disconnectLiveSplit();
|
speedrun::disconnectLiveSplit();
|
||||||
}
|
}
|
||||||
@@ -1330,8 +1375,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
|||||||
"Item drops such as rupees and hearts will never disappear after they drop.");
|
"Item drops such as rupees and hearts will never disappear after they drop.");
|
||||||
|
|
||||||
leftPane.add_section("Abilities");
|
leftPane.add_section("Abilities");
|
||||||
|
|
||||||
addCheat(
|
addCheat(
|
||||||
"Moon Jump (R+A)", getSettings().game.moonJump, "Hold R and A to rise into the air.");
|
"Moon Jump (R+A)", getSettings().game.moonJump, "Hold R and A to rise into the air.");
|
||||||
|
addCheat(
|
||||||
|
"Easy Quick Spin (R+B)", getSettings().game.easyQuickSpin, "Hold R to always do a Quick Spin when attacking with B.");
|
||||||
|
|
||||||
addCheat("Super Clawshot", getSettings().game.superClawshot,
|
addCheat("Super Clawshot", getSettings().game.superClawshot,
|
||||||
"Extends Clawshot behavior beyond the normal game rules.");
|
"Extends Clawshot behavior beyond the normal game rules.");
|
||||||
addCheat("Always Greatspin", getSettings().game.alwaysGreatspin,
|
addCheat("Always Greatspin", getSettings().game.alwaysGreatspin,
|
||||||
|
|||||||
+6
-19
@@ -31,9 +31,9 @@ void load_font(const char* filename, bool fallback = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool sInitialized = false;
|
bool sInitialized = false;
|
||||||
std::vector<std::unique_ptr<Document>> sDocumentStack;
|
std::vector<std::unique_ptr<Document> > sDocumentStack;
|
||||||
// Documents that don't participate in the focus stack
|
// Documents that don't participate in the focus stack
|
||||||
std::vector<std::unique_ptr<Document>> sPassiveDocuments;
|
std::vector<std::unique_ptr<Document> > sPassiveDocuments;
|
||||||
|
|
||||||
struct ScopedStyles {
|
struct ScopedStyles {
|
||||||
DocumentScope scope;
|
DocumentScope scope;
|
||||||
@@ -173,12 +173,10 @@ void handle_event(const SDL_Event& event) noexcept {
|
|||||||
const char* name = SDL_GetGamepadName(gamepad);
|
const char* name = SDL_GetGamepadName(gamepad);
|
||||||
Rml::String content = fmt::format("<span>{}</span>", name ? name : "[Unknown]");
|
Rml::String content = fmt::format("<span>{}</span>", name ? name : "[Unknown]");
|
||||||
Rml::String title = "Device Connected";
|
Rml::String title = "Device Connected";
|
||||||
if (const char* icon =
|
if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
|
||||||
connection_state_icon(SDL_GetGamepadConnectionState(gamepad)))
|
|
||||||
{
|
|
||||||
title = fmt::format(
|
title = fmt::format(
|
||||||
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>",
|
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>", title,
|
||||||
title, icon);
|
icon);
|
||||||
}
|
}
|
||||||
int batteryLevel = -1;
|
int batteryLevel = -1;
|
||||||
const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
|
const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
|
||||||
@@ -384,17 +382,6 @@ Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept {
|
|||||||
return parent->AppendChild(doc->CreateElement(tag));
|
return parent->AppendChild(doc->CreateElement(tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
Rml::Element* append_text(Rml::Element* parent, const Rml::String& text) noexcept {
|
|
||||||
if (parent == nullptr) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
auto* doc = parent->GetOwnerDocument();
|
|
||||||
if (doc == nullptr) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return parent->AppendChild(doc->CreateTextNode(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
NavCommand map_nav_event(const Rml::Event& event) noexcept {
|
NavCommand map_nav_event(const Rml::Event& event) noexcept {
|
||||||
const auto key = static_cast<Rml::Input::KeyIdentifier>(
|
const auto key = static_cast<Rml::Input::KeyIdentifier>(
|
||||||
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
||||||
@@ -461,7 +448,7 @@ void push_toast(Toast toast) noexcept {
|
|||||||
sToasts.push_back(std::move(toast));
|
sToasts.push_back(std::move(toast));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::unique_ptr<Document>>& get_document_stack() noexcept {
|
std::vector<std::unique_ptr<Document> >& get_document_stack() noexcept {
|
||||||
return sDocumentStack;
|
return sDocumentStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ Document* top_document() noexcept;
|
|||||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
|
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
|
||||||
std::string escape(std::string_view str) noexcept;
|
std::string escape(std::string_view str) noexcept;
|
||||||
Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept;
|
Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept;
|
||||||
Rml::Element* append_text(Rml::Element* parent, const Rml::String& text) noexcept;
|
|
||||||
|
|
||||||
NavCommand map_nav_event(const Rml::Event& event) noexcept;
|
NavCommand map_nav_event(const Rml::Event& event) noexcept;
|
||||||
Insets safe_area_insets(Rml::Context* context) noexcept;
|
Insets safe_area_insets(Rml::Context* context) noexcept;
|
||||||
|
|||||||
@@ -53,7 +53,6 @@
|
|||||||
#include "dusk/dusk.h"
|
#include "dusk/dusk.h"
|
||||||
#include "dusk/endian.h"
|
#include "dusk/endian.h"
|
||||||
#include "dusk/frame_interpolation.h"
|
#include "dusk/frame_interpolation.h"
|
||||||
#include "dusk/gfx.hpp"
|
|
||||||
#include "dusk/gx_helper.h"
|
#include "dusk/gx_helper.h"
|
||||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||||
#include "dusk/logging.h"
|
#include "dusk/logging.h"
|
||||||
@@ -2357,10 +2356,6 @@ int mDoGph_Painter() {
|
|||||||
|
|
||||||
GXSetClipMode(GX_CLIP_ENABLE);
|
GXSetClipMode(GX_CLIP_ENABLE);
|
||||||
|
|
||||||
#if TARGET_PC
|
|
||||||
dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_BEGIN, &camera_p->view, view_port);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// "drawing up to Background (Translucent) (Rendering)"
|
// "drawing up to Background (Translucent) (Rendering)"
|
||||||
fapGm_HIO_c::stopCpuTimer("背景(半透明)描画まで(レンダリング)");
|
fapGm_HIO_c::stopCpuTimer("背景(半透明)描画まで(レンダリング)");
|
||||||
@@ -2389,10 +2384,6 @@ int mDoGph_Painter() {
|
|||||||
|
|
||||||
GX_DEBUG_GROUP(dComIfGd_drawShadow, camera_p->view.viewMtx);
|
GX_DEBUG_GROUP(dComIfGd_drawShadow, camera_p->view.viewMtx);
|
||||||
|
|
||||||
#if TARGET_PC
|
|
||||||
dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_AFTER_TERRAIN, &camera_p->view, view_port);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// "shadow drawing (Rendering)"
|
// "shadow drawing (Rendering)"
|
||||||
fapGm_HIO_c::stopCpuTimer("影描画(レンダリング)");
|
fapGm_HIO_c::stopCpuTimer("影描画(レンダリング)");
|
||||||
@@ -2422,10 +2413,6 @@ int mDoGph_Painter() {
|
|||||||
|
|
||||||
GX_DEBUG_GROUP(dComIfGd_drawOpaListPacket);
|
GX_DEBUG_GROUP(dComIfGd_drawOpaListPacket);
|
||||||
|
|
||||||
#if TARGET_PC
|
|
||||||
dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_AFTER_OPAQUE, &camera_p->view, view_port);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// "drawing up to special-use drawing (Opaque) except J3D (Rendering)"
|
// "drawing up to special-use drawing (Opaque) except J3D (Rendering)"
|
||||||
fapGm_HIO_c::stopCpuTimer("J3D以外などの特殊用(不透明)描画まで(レンダリング)");
|
fapGm_HIO_c::stopCpuTimer("J3D以外などの特殊用(不透明)描画まで(レンダリング)");
|
||||||
@@ -2791,10 +2778,6 @@ int mDoGph_Painter() {
|
|||||||
captureScreenSetPort();
|
captureScreenSetPort();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if TARGET_PC
|
|
||||||
dusk::mods::gfx_run_stage(GFX_STAGE_FRAME_BEFORE_HUD);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (fapGmHIO_get2Ddraw()) {
|
if (fapGmHIO_get2Ddraw()) {
|
||||||
Mtx m4;
|
Mtx m4;
|
||||||
cMtx_copy(j3dSys.getViewMtx(), m4);
|
cMtx_copy(j3dSys.getViewMtx(), m4);
|
||||||
@@ -2852,10 +2835,6 @@ int mDoGph_Painter() {
|
|||||||
dComIfGd_draw2DXlu();
|
dComIfGd_draw2DXlu();
|
||||||
}
|
}
|
||||||
|
|
||||||
#if TARGET_PC
|
|
||||||
dusk::mods::gfx_run_stage(GFX_STAGE_FRAME_AFTER_HUD);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (dJcame_c::get()) {
|
if (dJcame_c::get()) {
|
||||||
dJcame_c::get()->show2D();
|
dJcame_c::get()->show2D();
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"id": "com.example.mod",
|
||||||
|
"name": "Template Mod",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "You",
|
||||||
|
"description": "An example Dusklight mod"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user