Compare commits

..

1 Commits

Author SHA1 Message Date
MelonSpeedruns c509ff7647 Easy Quick Spin Cheat 2026-07-10 09:22:17 -04:00
61 changed files with 438 additions and 5120 deletions
+1 -4
View File
@@ -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)
-5
View File
@@ -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
View File
@@ -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 ()
+1 -1
View File
@@ -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)
-11
View File
@@ -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
View File
@@ -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
+1 -1
-2
View File
@@ -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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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);
-10
View File
@@ -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
+1
View File
@@ -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;
-1
View File
@@ -37,6 +37,5 @@ struct SpeedrunInfo {
extern SpeedrunInfo m_speedrunInfo; extern SpeedrunInfo m_speedrunInfo;
void resetForSpeedrunMode(); void resetForSpeedrunMode();
void restoreFromSpeedrunMode();
} // namespace dusk } // namespace dusk
-64
View File
@@ -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
-209
View File
@@ -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 */
-20
View File
@@ -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
)
-7
View File
@@ -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."
}
-161
View File
@@ -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);
}
-108
View File
@@ -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));
}
-247
View File
@@ -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
-19
View File
@@ -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.
-21
View File
@@ -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.
-138
View File
@@ -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));
}
-931
View File
@@ -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;
}
}
-20
View File
@@ -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
)
-7
View File
@@ -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."
}
-262
View File
@@ -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
-7
View File
@@ -1,7 +0,0 @@
{
"id": "com.example.mod",
"name": "Template Mod",
"version": "1.0.0",
"author": "You",
"description": "An example Dusklight mod"
}
-1
View File
@@ -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
View File
@@ -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")
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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;
} }
+1 -2
View File
@@ -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
View File
@@ -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;
-13
View File
@@ -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) {
-134
View File
@@ -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
+88 -66
View File
@@ -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{
-798
View File
@@ -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
View File
@@ -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");
} }
} }
} }
+37 -36
View File
@@ -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;
} }
} }
-2
View File
@@ -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);
-2
View File
@@ -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
-188
View File
@@ -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
View File
@@ -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;
} }
} }
+3
View File
@@ -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);
-12
View File
@@ -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
View File
@@ -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() {
+2 -2
View File
@@ -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;
} }
+52 -3
View File
@@ -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
View File
@@ -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;
} }
-1
View File
@@ -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;
-21
View File
@@ -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();
+7
View File
@@ -0,0 +1,7 @@
{
"id": "com.example.mod",
"name": "Template Mod",
"version": "1.0.0",
"author": "You",
"description": "An example Dusklight mod"
}