mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-30 23:44:28 -04:00
Merge branch 'main' of ssh://github.com/TwilitRealm/dusklight into rando-mod
This commit is contained in:
@@ -208,9 +208,6 @@ jobs:
|
||||
- name: Build bundled mods
|
||||
run: cmake --build --preset ${{matrix.preset}} --target dusklight_mods
|
||||
|
||||
- name: Stage stripped JNI library
|
||||
run: ANDROID_STAGE_ABIS="${{matrix.abi}}" platforms/android/scripts/stage-jni-libs.sh
|
||||
|
||||
- name: Build APK
|
||||
working-directory: platforms/android
|
||||
run: ./gradlew :app:assembleRelease --rerun-tasks
|
||||
|
||||
@@ -610,6 +610,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR
|
||||
add_subdirectory(mods/template_mod)
|
||||
add_subdirectory(mods/ao_mod)
|
||||
add_subdirectory(mods/shadow_mod)
|
||||
add_subdirectory(mods/window_demo)
|
||||
add_subdirectory(mods/randomizer)
|
||||
endif ()
|
||||
|
||||
|
||||
@@ -62,3 +62,7 @@ target_sources(dusklight_mod_feature_game INTERFACE
|
||||
add_library(dusklight_mod_feature_webgpu INTERFACE)
|
||||
target_link_libraries(dusklight_mod_feature_webgpu INTERFACE dusklight_mod_api)
|
||||
target_compile_definitions(dusklight_mod_feature_webgpu INTERFACE DUSK_MOD_FEATURE_WEBGPU=1)
|
||||
|
||||
add_library(dusklight_mod_feature_fmt INTERFACE)
|
||||
target_link_libraries(dusklight_mod_feature_fmt INTERFACE dusklight_mod_api)
|
||||
target_compile_definitions(dusklight_mod_feature_fmt INTERFACE DUSK_MOD_FEATURE_FMT=1)
|
||||
|
||||
+27
-1
@@ -113,6 +113,29 @@ function(_mod_add_webgpu_headers target_name)
|
||||
endif ()
|
||||
endfunction()
|
||||
|
||||
function(_mod_add_fmt target_name)
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
find_package(fmt 11 CONFIG QUIET GLOBAL)
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
include(FetchContent)
|
||||
message(STATUS "Mod SDK: fetching fmt")
|
||||
# Keep the fallback version in sync with extern/aurora/extern/CMakeLists.txt.
|
||||
FetchContent_Declare(fmt
|
||||
URL https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz
|
||||
URL_HASH SHA256=ea7de4299689e12b6dddd392f9896f08fb0777ac7168897a244a6d6085043fea
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL)
|
||||
FetchContent_MakeAvailable(fmt)
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
message(FATAL_ERROR "add_mod: FEATURES fmt could not provide fmt::fmt-header-only")
|
||||
endif ()
|
||||
target_link_libraries(${target_name} PRIVATE fmt::fmt-header-only)
|
||||
endfunction()
|
||||
|
||||
function(add_mod target_name)
|
||||
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR"
|
||||
"SOURCES;RUNTIME_LIBRARIES;FEATURES" ${ARGN})
|
||||
@@ -127,7 +150,7 @@ function(add_mod target_name)
|
||||
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
|
||||
endif ()
|
||||
|
||||
set(_supported_features game webgpu)
|
||||
set(_supported_features fmt game webgpu)
|
||||
set(_features "")
|
||||
foreach (_feature IN LISTS ARG_FEATURES)
|
||||
list(FIND _supported_features "${_feature}" _feature_index)
|
||||
@@ -167,6 +190,9 @@ function(add_mod target_name)
|
||||
if (_feature STREQUAL "webgpu")
|
||||
_mod_add_webgpu_headers(${target_name})
|
||||
endif ()
|
||||
if (_feature STREQUAL "fmt")
|
||||
_mod_add_fmt(${target_name})
|
||||
endif ()
|
||||
if (_feature STREQUAL "game" OR _feature STREQUAL "webgpu")
|
||||
set(_needs_host_link TRUE)
|
||||
endif ()
|
||||
|
||||
@@ -2,40 +2,50 @@ include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||
|
||||
set(_SYMGEN_VERSION "1.3.1")
|
||||
set(_SYMGEN_VERSION "1.3.2")
|
||||
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
|
||||
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
|
||||
mark_as_advanced(SYMGEN_PATH)
|
||||
|
||||
function(symgen_host_asset out_name)
|
||||
function(symgen_host_asset out_name out_hash)
|
||||
string(TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" _host_processor)
|
||||
set(_asset "")
|
||||
set(_asset_hash "")
|
||||
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
|
||||
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||
set(_asset "symgen-macos-arm64")
|
||||
set(_asset_hash "SHA256=0344838d1674df09c17c3eeddcf26eb89c333fdb85dfd78f68adc436070eccbe")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-macos-x86_64")
|
||||
set(_asset_hash "SHA256=ae0674f4a1e9d0dedfa02d35939ac28cdd229276b331c1f507df5df809cbec7e")
|
||||
endif ()
|
||||
elseif (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||
if (_host_processor MATCHES "^(aarch64|arm64)$")
|
||||
set(_asset "symgen-linux-aarch64")
|
||||
set(_asset_hash "SHA256=af766de2bfaeb0a06f6d7bc17bb2510b4a9c40f44a56e49bc4a4b798a6223042")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-linux-x86_64")
|
||||
set(_asset_hash "SHA256=ebd62fb9623acc942b6295609e2306f85a73043b0a8a117f2072b761dd08e68f")
|
||||
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_asset "symgen-linux-i686")
|
||||
set(_asset_hash "SHA256=07780a4513fd29726578efc4ff2d88736b22f407ed821b32a68e35ff1e9af5f4")
|
||||
endif ()
|
||||
elseif (CMAKE_HOST_WIN32)
|
||||
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||
set(_asset "symgen-windows-arm64.exe")
|
||||
set(_asset_hash "SHA256=5bb22b4a4a9b5ad45646af411bfb09b8321a732ff3a077eb4c9de1feaef27d2b")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-windows-x86_64.exe")
|
||||
set(_asset_hash "SHA256=1d1ac087f991a96932d108969e15998becfec643e88f3e7d182ca97ebfc6a46f")
|
||||
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_asset "symgen-windows-x86.exe")
|
||||
set(_asset_hash "SHA256=c113f4cd05f813efe2b1878dbfcf44d6302aee3974271736e0036430b0cced78")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
set(${out_name} "${_asset}" PARENT_SCOPE)
|
||||
set(${out_hash} "${_asset_hash}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(ensure_symgen required)
|
||||
@@ -54,7 +64,7 @@ function(ensure_symgen required)
|
||||
return()
|
||||
endif ()
|
||||
else ()
|
||||
symgen_host_asset(_asset)
|
||||
symgen_host_asset(_asset _asset_hash)
|
||||
if (_asset STREQUAL "")
|
||||
if (required)
|
||||
message(FATAL_ERROR "symgen: no prebuilt binary for host "
|
||||
@@ -75,7 +85,8 @@ function(ensure_symgen required)
|
||||
file(DOWNLOAD "${_url}" "${_symgen}"
|
||||
TLS_VERIFY ON
|
||||
STATUS _download_status
|
||||
SHOW_PROGRESS)
|
||||
SHOW_PROGRESS
|
||||
EXPECTED_HASH "${_asset_hash}")
|
||||
list(GET _download_status 0 _download_code)
|
||||
if (NOT _download_code EQUAL 0)
|
||||
list(GET _download_status 1 _download_message)
|
||||
@@ -106,6 +117,16 @@ function(setup_symbol_manifest target)
|
||||
endif ()
|
||||
add_dependencies(${target} symgen)
|
||||
|
||||
# Reserve an ELF program-header entry when the linker supports it (mold).
|
||||
# symgen can replace the PT_NULL entry without relocating the table, keeping later post-link tools safe.
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
include(CheckLinkerFlag)
|
||||
check_linker_flag(CXX "LINKER:--spare-program-headers=1" _linker_supports_spare_program_headers)
|
||||
if (_linker_supports_spare_program_headers)
|
||||
target_link_options(${target} PRIVATE "LINKER:--spare-program-headers=1")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (WIN32)
|
||||
set(_input --pdb "$<TARGET_PDB_FILE:${target}>")
|
||||
else ()
|
||||
|
||||
+83
-11
@@ -54,7 +54,7 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchDusklight.cmake")
|
||||
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
|
||||
|
||||
add_mod(my_mod
|
||||
FEATURES game # remove for service/asset-only mods; add webgpu for GfxService
|
||||
FEATURES game fmt # remove game for service-only mods; add webgpu for GfxService
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res # mod resources, including icon.png and banner.png
|
||||
@@ -64,6 +64,8 @@ add_mod(my_mod
|
||||
```
|
||||
|
||||
Available features:
|
||||
|
||||
- `fmt`: Provides the header-only `{fmt}` library and the formatted logging helpers in `mods/svc/log.hpp`.
|
||||
- `game`: Allows calling into and hooking game code. Mods that **only** use services may omit it, providing a wider
|
||||
range of compatibility with Dusklight versions and a slightly faster build process.
|
||||
- `webgpu`: Allows importing the WebGPU API (`webgpu/webgpu.h`). Must be enabled when using
|
||||
@@ -143,6 +145,9 @@ IMPORT_SERVICE_VERSION(LogService, svc_log, 0); // required, minimum minor ver
|
||||
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
|
||||
```
|
||||
|
||||
A service must be imported in only **one** file (usually your `mod.cpp`). Other files may simply use `svc_log` or
|
||||
`mods::log::` after including the appropriate header.
|
||||
|
||||
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
|
||||
allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major**
|
||||
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
|
||||
@@ -179,7 +184,15 @@ svc_log->write(mod_ctx, LOG_LEVEL_DEBUG, "verbose details");
|
||||
```
|
||||
|
||||
Messages appear in the console prefixed with your mod ID. Messages are plain UTF-8 strings and are copied before the
|
||||
call returns; use `snprintf` or `fmt::format` for formatting.
|
||||
call returns. C++ mods can enable `add_mod(... FEATURES fmt)` and use the formatted logging helpers in
|
||||
`mods/svc/log.hpp`:
|
||||
|
||||
```cpp
|
||||
#include <mods/svc/log.hpp>
|
||||
|
||||
mods::log::info("spawned actor {} at ({}, {})", actorName, x, y);
|
||||
mods::log::warn("health is down to {:.1f}%", healthPercent);
|
||||
```
|
||||
|
||||
### ResourceService (`mods/svc/resource.h`)
|
||||
|
||||
@@ -236,7 +249,7 @@ every service dropped its state. For your own mod's teardown, use `mod_shutdown`
|
||||
### HookService (`mods/svc/hook.h`)
|
||||
|
||||
Installs hooks on game functions and resolves symbols by name. You'll rarely call it directly; use the typed helpers in
|
||||
`mods/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||
`mods/svc/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||
|
||||
### OverlayService (`mods/svc/overlay.h`)
|
||||
|
||||
@@ -409,6 +422,22 @@ sets `keep_open`. A `keep_open` action can close it later (or immediately) with
|
||||
`on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live
|
||||
dialog.
|
||||
|
||||
**Toasts:** `push_toast` enqueues a notification. Titles and bodies accept RML. The optional `type` is applied as an
|
||||
RCSS class; `warning` uses the built-in warning appearance, and mods can define their own types. A duration of 0 uses
|
||||
the default of 5 seconds.
|
||||
|
||||
Toasts have a `mod-id` attribute, so `UI_SCOPE_OVERLAY` styles can use selectors such as
|
||||
`toast[mod-id="com.example.randomizer"].success`.
|
||||
|
||||
```cpp
|
||||
UiToastDesc toast = UI_TOAST_DESC_INIT;
|
||||
toast.type = "success";
|
||||
toast.title_rml = "Randomizer";
|
||||
toast.body_rml = "<span>Seed loaded successfully.</span>";
|
||||
toast.duration_ms = 3000;
|
||||
svc_ui->push_toast(mod_ctx, &toast);
|
||||
```
|
||||
|
||||
**Menu bar tabs:** `register_menu_tab` adds a tab to the in-game menu bar. `on_selected` fires when the user activates
|
||||
the tab: typically you'd push a window from it. The tab is removed by `unregister_menu_tab`, or automatically when the
|
||||
mod is disabled.
|
||||
@@ -420,6 +449,26 @@ 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`,
|
||||
unless changing host UI is intentional.
|
||||
|
||||
### WindowService (`mods/svc/window.h`)
|
||||
|
||||
Allows creating new windows that can be rendered to via `GfxService`.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
|
||||
WindowDesc desc = WINDOW_DESC_INIT;
|
||||
desc.title = "My auxiliary view";
|
||||
desc.on_event = on_window_event;
|
||||
WindowHandle window = 0;
|
||||
svc_window->create_window(mod_ctx, &desc, &window);
|
||||
```
|
||||
|
||||
Window callbacks run on the game thread. A close event is only a request; call `destroy_window` when the mod is ready to
|
||||
close it. A window attached to a GfxService present target cannot be destroyed until that target is unregistered. Only
|
||||
one present target may be attached to a WindowService window at a time.
|
||||
|
||||
New windows are hidden by default so a mod can finish attaching graphics before calling `show_window`.
|
||||
|
||||
### GfxService (`mods/svc/gfx.h`)
|
||||
|
||||
**Requires `add_mod(... FEATURES webgpu)`**
|
||||
@@ -451,6 +500,30 @@ registered with `register_compute_type` follow the same worker-thread rule and r
|
||||
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`.
|
||||
|
||||
#### External presentation
|
||||
|
||||
GfxService supports external presentation ("present targets") backed by either a WindowService window (via
|
||||
`register_window_present_target`) or a plain `WGPUSurface` (via `register_present_target`).
|
||||
|
||||
```cpp
|
||||
GfxPresentTargetDesc target_desc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
target_desc.render = render_auxiliary_view;
|
||||
GfxPresentTargetHandle target = 0;
|
||||
svc_gfx->register_window_present_target(mod_ctx, window, &target_desc, &target);
|
||||
|
||||
// From a stage callback:
|
||||
svc_gfx->push_present(mod_ctx, target, &payload, sizeof(payload));
|
||||
```
|
||||
|
||||
For WindowService windows, the surface is automatically reconfigured on window size changes.
|
||||
For plain `WBPUSurface`s, `resize_present_target` must be used to resize.
|
||||
|
||||
To create a `WGPUSurface` manually, `GfxDeviceInfo` holds the `WGPUInstance` and `WGPUAdapter` which can be used with
|
||||
`wgpuInstanceCreateSurface` and a chained `WGPUSurfaceSource*` struct.
|
||||
|
||||
`push_present` must be called every frame from a GfxService stage callback. If surface was lost, `push_present` returns
|
||||
`MOD_ERROR`. Unregister and re-register the target before trying again.
|
||||
|
||||
### CameraService (`mods/svc/camera.h`)
|
||||
|
||||
Converts a game view provided by a render callback into WebGPU-convention camera data. Matrix fields are column-major
|
||||
@@ -477,11 +550,10 @@ first in-game frame. Projection matrices match the renderer's WebGPU clip conven
|
||||
**Requires `add_mod(... FEATURES game)`**
|
||||
|
||||
Mods may hook the vast majority of game functions, including file-local static, private and virtual functions.
|
||||
`mods/hook.hpp` provides typed helpers over the hook service:
|
||||
`mods/svc/hook.hpp` provides typed helpers over the hook service:
|
||||
|
||||
```cpp
|
||||
#include "mods/hook.hpp"
|
||||
#include "mods/svc/hook.h"
|
||||
#include "mods/svc/hook.hpp"
|
||||
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
|
||||
@@ -505,7 +577,7 @@ HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
mods::hook_add_pre<LinkPosMove>(svc_hook, on_pos_move_pre);
|
||||
mods::hook::add_pre<LinkPosMove>(on_pos_move_pre);
|
||||
```
|
||||
|
||||
### Post-hooks
|
||||
@@ -516,7 +588,7 @@ if any.
|
||||
```cpp
|
||||
void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... }
|
||||
|
||||
mods::hook_add_post<LinkPosMove>(svc_hook, on_pos_move_post);
|
||||
mods::hook::add_post<LinkPosMove>(on_pos_move_post);
|
||||
```
|
||||
|
||||
### Replace-hooks
|
||||
@@ -531,7 +603,7 @@ void on_execute_replace(ModContext*, void* args, void* retval, void*) {
|
||||
}
|
||||
}
|
||||
|
||||
mods::hook_replace<LinkExecute>(svc_hook, on_execute_replace);
|
||||
mods::hook::replace<LinkExecute>(on_execute_replace);
|
||||
```
|
||||
|
||||
By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`,
|
||||
@@ -547,7 +619,7 @@ symbol name instead. You must supply the signature along with the name.
|
||||
DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
|
||||
void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
|
||||
|
||||
mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit_pre);
|
||||
mods::hook::add_pre<HookshotHit>(on_hookshot_hit_pre);
|
||||
...
|
||||
HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the original
|
||||
```
|
||||
@@ -587,7 +659,7 @@ HookAction on_create_item_pre(ModContext*, void* args, void*, void*) {
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
mods::hook_add_pre<CreateItem>(svc_hook, on_create_item_pre);
|
||||
mods::hook::add_pre<CreateItem>(on_create_item_pre);
|
||||
```
|
||||
|
||||
For reference parameters (e.g. `const cXyz& pos`), `arg_ref<cXyz>` yields a direct reference.
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 2bbb1229a9...0bddb86249
@@ -1500,6 +1500,8 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/texture.cpp
|
||||
src/dusk/mods/svc/ui.cpp
|
||||
src/dusk/mods/svc/ui.hpp
|
||||
src/dusk/mods/svc/window.cpp
|
||||
src/dusk/mods/svc/window.hpp
|
||||
src/dusk/mouse.cpp
|
||||
src/dusk/scope_guard.hpp
|
||||
src/dusk/settings.cpp
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
description = "Dusklight — native PC port of the Twilight Princess decompilation";
|
||||
|
||||
inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
inputs.self.submodules = true;
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs }:
|
||||
@@ -58,21 +59,6 @@
|
||||
hasNodPrebuilt = nodPrebuiltInfo ? ${system};
|
||||
|
||||
aurora = builtins.pathExists "${self}/extern/aurora/CMakeLists.txt";
|
||||
needSubmodules = ''
|
||||
dusklight: The aurora submodule is not vendored. Add submodules=1 to build.
|
||||
|
||||
As a flake input:
|
||||
|
||||
dusklight.url = "git+https://github.com/TwilitRealm/dusklight?ref=main&submodules=1";
|
||||
|
||||
nix command:
|
||||
|
||||
nix run 'git+https://github.com/TwilitRealm/dusklight?submodules=1'
|
||||
|
||||
Local checkout:
|
||||
|
||||
nix run '.?submodules=1#dusklight'
|
||||
'';
|
||||
|
||||
dawn = pkgs.fetchzip {
|
||||
url = "https://github.com/encounter/dawn/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
|
||||
@@ -140,6 +126,14 @@
|
||||
JSON = pkgs.nlohmann_json.src;
|
||||
XXHASH = pkgs.xxhash.src;
|
||||
ZSTD = pkgs.zstd.src;
|
||||
|
||||
|
||||
MINIZ = pkgs.fetchzip {
|
||||
url = "https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip";
|
||||
hash = "sha256-DXysXkQEmoDAMMg1F8KexkwpXNyiHNzLJqXR9SMEkxk=";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
FMT = pkgs.fetchzip {
|
||||
url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz";
|
||||
hash = "sha256-ZmI1Dv0ZabPlxa02OpERI47jp7zFfjpeWCy1WyuPYZ0=";
|
||||
@@ -165,19 +159,16 @@
|
||||
};
|
||||
|
||||
dusklight =
|
||||
if !aurora then
|
||||
throw needSubmodules
|
||||
else
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "dusklight";
|
||||
version = versionSuffix;
|
||||
src = ./.;
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "dusklight";
|
||||
version = versionSuffix;
|
||||
src = ./.;
|
||||
|
||||
postUnpack = ''
|
||||
chmod -R u+w "$sourceRoot"
|
||||
substituteInPlace "$sourceRoot/extern/aurora/CMakeLists.txt" \
|
||||
--replace-warn "add_subdirectory(tests)" ""
|
||||
'';
|
||||
postUnpack = ''
|
||||
chmod -R u+w "$sourceRoot"
|
||||
substituteInPlace "$sourceRoot/extern/aurora/CMakeLists.txt" \
|
||||
--replace-warn "add_subdirectory(tests)" ""
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.shadow_mod",
|
||||
"name": "[Demo] Dynamic Shadows",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"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."
|
||||
}
|
||||
|
||||
+276
-38
@@ -20,7 +20,7 @@
|
||||
#include "dolphin/gx/GXPixel.h"
|
||||
#include "dolphin/gx/GXTransform.h"
|
||||
#include "m_Do/m_Do_mtx.h"
|
||||
#include "mods/hook.hpp"
|
||||
#include "mods/svc/hook.hpp"
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/camera.h"
|
||||
#include "mods/svc/config.h"
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "mods/svc/log.h"
|
||||
#include "mods/svc/resource.h"
|
||||
#include "mods/svc/ui.h"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -45,6 +46,7 @@ IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
IMPORT_SERVICE(CameraService, svc_camera);
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -62,6 +64,7 @@ ConfigVarHandle g_cvarDebugView = 0;
|
||||
GfxDrawTypeHandle g_drawType = 0;
|
||||
GfxStageHookHandle g_sceneBeginHook = 0;
|
||||
GfxStageHookHandle g_sceneAfterTerrainHook = 0;
|
||||
GfxStageHookHandle g_sceneAfterOpaqueHook = 0;
|
||||
GfxStageHookHandle g_frameBeforeHudHook = 0;
|
||||
UiWindowHandle g_controlsWindow = 0;
|
||||
ResourceBuffer g_shaderSource = RESOURCE_BUFFER_INIT;
|
||||
@@ -70,6 +73,11 @@ WGPURenderPipeline g_compositePipeline = nullptr; // multiply blend
|
||||
WGPURenderPipeline g_compositeDebugPipeline = nullptr; // no blend (debug views)
|
||||
WGPUBindGroupLayout g_compositeLayout = nullptr;
|
||||
WGPUBindGroupLayout g_compositeDebugLayout = nullptr;
|
||||
WGPURenderPipeline g_debugPresentPipeline = nullptr;
|
||||
WGPUBindGroupLayout g_debugPresentLayout = nullptr;
|
||||
WGPUTextureFormat g_debugPresentFormat = WGPUTextureFormat_Undefined;
|
||||
WindowHandle g_debugWindow = 0;
|
||||
GfxPresentTargetHandle g_debugPresentTarget = 0;
|
||||
|
||||
struct MapPassOutput {
|
||||
bool ready = false;
|
||||
@@ -187,6 +195,34 @@ int64_t get_debug_mode() {
|
||||
return std::clamp<int64_t>(get_int_option(g_cvarDebugView, 0), 0, 10);
|
||||
}
|
||||
|
||||
bool debug_window_open() {
|
||||
return g_debugWindow != 0 && g_debugPresentTarget != 0;
|
||||
}
|
||||
|
||||
ModResult close_debug_window() {
|
||||
if (g_debugPresentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_debugPresentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_debugPresentTarget = 0;
|
||||
}
|
||||
if (g_debugWindow != 0) {
|
||||
const auto result = svc_window->destroy_window(mod_ctx, g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_debugWindow = 0;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void on_debug_window_event(ModContext*, WindowHandle, const WindowEvent* event, void*) {
|
||||
if (event->type == WINDOW_EVENT_CLOSE_REQUESTED && close_debug_window() != MOD_OK) {
|
||||
svc_log->error(mod_ctx, "failed to close shadow debug window");
|
||||
}
|
||||
}
|
||||
|
||||
bool matrix_ready(const Mtx m) {
|
||||
float basis = 0.0f;
|
||||
for (int r = 0; r < 3; ++r) {
|
||||
@@ -395,6 +431,90 @@ bool build_composite_pipeline(
|
||||
return outLayout != nullptr;
|
||||
}
|
||||
|
||||
void release_debug_present_pipeline() {
|
||||
if (g_debugPresentPipeline != nullptr) {
|
||||
wgpuRenderPipelineRelease(g_debugPresentPipeline);
|
||||
g_debugPresentPipeline = nullptr;
|
||||
}
|
||||
if (g_debugPresentLayout != nullptr) {
|
||||
wgpuBindGroupLayoutRelease(g_debugPresentLayout);
|
||||
g_debugPresentLayout = nullptr;
|
||||
}
|
||||
g_debugPresentFormat = WGPUTextureFormat_Undefined;
|
||||
}
|
||||
|
||||
bool ensure_debug_present_pipeline(const GfxPresentContext& ctx) {
|
||||
if (g_debugPresentPipeline != nullptr && g_debugPresentFormat == ctx.target_format) {
|
||||
return true;
|
||||
}
|
||||
release_debug_present_pipeline();
|
||||
|
||||
WGPUShaderSourceWGSL wgsl = WGPU_SHADER_SOURCE_WGSL_INIT;
|
||||
wgsl.code = {static_cast<const char*>(g_shaderSource.data), g_shaderSource.size};
|
||||
WGPUShaderModuleDescriptor moduleDesc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT;
|
||||
moduleDesc.nextInChain = &wgsl.chain;
|
||||
moduleDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
WGPUShaderModule module = wgpuDeviceCreateShaderModule(ctx.device, &moduleDesc);
|
||||
if (module == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT;
|
||||
colorTarget.format = ctx.target_format;
|
||||
WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT;
|
||||
fragment.module = module;
|
||||
fragment.entryPoint = {"fs_main", WGPU_STRLEN};
|
||||
fragment.targetCount = 1;
|
||||
fragment.targets = &colorTarget;
|
||||
|
||||
WGPURenderPipelineDescriptor pipelineDesc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT;
|
||||
pipelineDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
pipelineDesc.vertex.module = module;
|
||||
pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN};
|
||||
pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
pipelineDesc.multisample.count = 1;
|
||||
pipelineDesc.fragment = &fragment;
|
||||
g_debugPresentPipeline = wgpuDeviceCreateRenderPipeline(ctx.device, &pipelineDesc);
|
||||
wgpuShaderModuleRelease(module);
|
||||
if (g_debugPresentPipeline == nullptr) {
|
||||
return false;
|
||||
}
|
||||
g_debugPresentLayout = wgpuRenderPipelineGetBindGroupLayout(g_debugPresentPipeline, 0);
|
||||
if (g_debugPresentLayout == nullptr) {
|
||||
release_debug_present_pipeline();
|
||||
return false;
|
||||
}
|
||||
g_debugPresentFormat = ctx.target_format;
|
||||
return true;
|
||||
}
|
||||
|
||||
WGPUBindGroup create_composite_bind_group(WGPUDevice device, WGPUBindGroupLayout layout,
|
||||
WGPUBuffer uniformBuffer, const DrawPayload& data) {
|
||||
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
|
||||
layout == nullptr || uniformBuffer == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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.sceneDepth;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = data.shadowMap;
|
||||
entries[2].binding = 2;
|
||||
entries[2].buffer = uniformBuffer;
|
||||
entries[2].offset = data.uniform_offset;
|
||||
entries[2].size = data.uniform_size;
|
||||
entries[3].binding = 3;
|
||||
entries[3].textureView = data.lightColor;
|
||||
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
|
||||
bindGroupDesc.layout = layout;
|
||||
bindGroupDesc.entryCount = 4;
|
||||
bindGroupDesc.entries = entries;
|
||||
return wgpuDeviceCreateBindGroup(device, &bindGroupDesc);
|
||||
}
|
||||
|
||||
// Render worker thread: fullscreen deferred-shadow composite.
|
||||
void on_draw(
|
||||
ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
@@ -407,29 +527,12 @@ void on_draw(
|
||||
WGPURenderPipeline pipeline =
|
||||
data.debug_mode != 0 ? g_compositeDebugPipeline : g_compositePipeline;
|
||||
WGPUBindGroupLayout layout = data.debug_mode != 0 ? g_compositeDebugLayout : g_compositeLayout;
|
||||
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
|
||||
pipeline == nullptr)
|
||||
{
|
||||
if (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.sceneDepth;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = data.shadowMap;
|
||||
entries[2].binding = 2;
|
||||
entries[2].buffer = ctx->uniform_buffer;
|
||||
entries[2].offset = data.uniform_offset;
|
||||
entries[2].size = data.uniform_size;
|
||||
entries[3].binding = 3;
|
||||
entries[3].textureView = data.lightColor;
|
||||
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
|
||||
bindGroupDesc.layout = layout;
|
||||
bindGroupDesc.entryCount = 4;
|
||||
bindGroupDesc.entries = entries;
|
||||
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
|
||||
WGPUBindGroup bindGroup =
|
||||
create_composite_bind_group(ctx->device, layout, ctx->uniform_buffer, data);
|
||||
if (bindGroup == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -440,6 +543,81 @@ void on_draw(
|
||||
wgpuBindGroupRelease(bindGroup);
|
||||
}
|
||||
|
||||
// Render worker thread: draw the selected diagnostic into the auxiliary surface.
|
||||
void on_debug_present(
|
||||
ModContext*, const GfxPresentContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
WGPUBindGroup bindGroup = nullptr;
|
||||
if (payloadSize == sizeof(DrawPayload)) {
|
||||
DrawPayload data;
|
||||
std::memcpy(&data, payload, sizeof(data));
|
||||
if (ensure_debug_present_pipeline(*ctx)) {
|
||||
bindGroup = create_composite_bind_group(
|
||||
ctx->device, g_debugPresentLayout, ctx->uniform_buffer, data);
|
||||
}
|
||||
}
|
||||
|
||||
WGPURenderPassColorAttachment colorAttachment = WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT;
|
||||
colorAttachment.view = ctx->target_view;
|
||||
colorAttachment.loadOp = WGPULoadOp_Clear;
|
||||
colorAttachment.storeOp = WGPUStoreOp_Store;
|
||||
colorAttachment.clearValue = WGPUColor{0.0, 0.0, 0.0, 1.0};
|
||||
WGPURenderPassDescriptor passDesc = WGPU_RENDER_PASS_DESCRIPTOR_INIT;
|
||||
passDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
passDesc.colorAttachmentCount = 1;
|
||||
passDesc.colorAttachments = &colorAttachment;
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(ctx->encoder, &passDesc);
|
||||
|
||||
if (bindGroup != nullptr) {
|
||||
wgpuRenderPassEncoderSetPipeline(pass, g_debugPresentPipeline);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, bindGroup, 0, nullptr);
|
||||
wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
|
||||
wgpuBindGroupRelease(bindGroup);
|
||||
}
|
||||
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
ModResult open_debug_window() {
|
||||
if (g_debugWindow != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
WindowDesc windowDesc = WINDOW_DESC_INIT;
|
||||
windowDesc.title = "Shadow Debug View";
|
||||
windowDesc.width = 720;
|
||||
windowDesc.height = 480;
|
||||
windowDesc.on_event = on_debug_window_event;
|
||||
auto result = svc_window->create_window(mod_ctx, &windowDesc, &g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
GfxPresentTargetDesc presentDesc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
presentDesc.label = "Shadow debug surface";
|
||||
presentDesc.render = on_debug_present;
|
||||
result = svc_gfx->register_window_present_target(
|
||||
mod_ctx, g_debugWindow, &presentDesc, &g_debugPresentTarget);
|
||||
if (result != MOD_OK) {
|
||||
close_debug_window();
|
||||
return result;
|
||||
}
|
||||
|
||||
result = svc_window->show_window(mod_ctx, g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
close_debug_window();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void on_toggle_debug_window(ModContext*, void*) {
|
||||
const auto result = debug_window_open() ? close_debug_window() : open_debug_window();
|
||||
if (result != MOD_OK) {
|
||||
svc_log->error(mod_ctx, debug_window_open() ? "failed to close shadow debug window" :
|
||||
"failed to open shadow debug window");
|
||||
}
|
||||
}
|
||||
|
||||
// Picks the sun or moon (whichever is above the horizon) and returns the normalized
|
||||
// world-space direction *toward* the light plus a horizon fade factor. False = no light.
|
||||
bool compute_light(float outDirToLight[3], float& outFade) {
|
||||
@@ -595,7 +773,7 @@ void restore_actual_light_debug() {
|
||||
void on_scene_begin(ModContext*, const GfxStageContext* stageCtx, void*) {
|
||||
restore_actual_light_debug();
|
||||
capture_scene_camera(stageCtx);
|
||||
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9) {
|
||||
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9 || debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -653,7 +831,7 @@ void render_shadow_map(
|
||||
return;
|
||||
}
|
||||
const int64_t debugMode = get_debug_mode();
|
||||
if (debugMode == 9) {
|
||||
if (debugMode == 9 && !debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
if (!matrix_ready(replayView)) {
|
||||
@@ -749,19 +927,30 @@ void render_shadow_map(
|
||||
g_mapPass.ready = true;
|
||||
}
|
||||
|
||||
// Game thread, after the full 3D scene: deferred composite.
|
||||
void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
// Game thread, after opaque scene draws and before translucent/fog overlays: deferred composite.
|
||||
void on_scene_after_opaque(ModContext*, const GfxStageContext*, void*) {
|
||||
const int64_t debugMode = get_debug_mode();
|
||||
const bool presentDebug = debug_window_open();
|
||||
restore_actual_light_debug();
|
||||
|
||||
if (presentDebug && debugMode == 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
|
||||
const MapPassOutput mapPass = std::exchange(g_mapPass, {});
|
||||
if (debugMode == 9) {
|
||||
if (debugMode == 9 && !debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
if (!mapPass.ready || mapPass.shadowMap == nullptr || mapPass.lightColor == nullptr) {
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!g_sceneCamera.valid) {
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const CameraInfo& camera = g_sceneCamera.info;
|
||||
@@ -773,6 +962,9 @@ void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK ||
|
||||
resolved.depth == nullptr)
|
||||
{
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -797,7 +989,7 @@ void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
uniforms.inv_size[0] = 1.0f / uniforms.size[0];
|
||||
uniforms.inv_size[1] = 1.0f / uniforms.size[1];
|
||||
uniforms.edge_fade_width =
|
||||
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarEdgeFadeWidth, 32), 0, 128));
|
||||
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarEdgeFadeWidth, 32), 0, 256));
|
||||
uniforms.strength =
|
||||
mapPass.fade *
|
||||
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarStrength, 45), 0, 100)) /
|
||||
@@ -806,15 +998,36 @@ void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
uniforms.contact_enabled = get_bool_option(g_cvarContactShadows, false) ? 1.0f : 0.0f;
|
||||
uniforms.contact_thickness = 25.0f;
|
||||
uniforms.contact_length = 60.0f;
|
||||
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
|
||||
// Camera Replay intentionally uses the gameplay-camera offscreen pass instead of the light
|
||||
// shadow map, so it remains diagnostic on both windows. Other external diagnostics leave the
|
||||
// main window on the normal shadow composite.
|
||||
uniforms.debug_mode = presentDebug && debugMode != 10 ? 0u : static_cast<uint32_t>(debugMode);
|
||||
|
||||
GfxRange uniformRange{0, 0};
|
||||
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &uniformRange) != MOD_OK) {
|
||||
return;
|
||||
}
|
||||
const DrawPayload payload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
|
||||
uniformRange.offset, uniformRange.size, static_cast<uint32_t>(debugMode)};
|
||||
uniformRange.offset, uniformRange.size, uniforms.debug_mode};
|
||||
svc_gfx->push_draw(mod_ctx, g_drawType, &payload, sizeof(payload));
|
||||
|
||||
if (presentDebug && debugMode != 0) {
|
||||
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
|
||||
GfxRange debugUniformRange{0, 0};
|
||||
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &debugUniformRange) !=
|
||||
MOD_OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const DrawPayload debugPayload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
|
||||
debugUniformRange.offset, debugUniformRange.size, uniforms.debug_mode};
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, &debugPayload, sizeof(debugPayload));
|
||||
}
|
||||
}
|
||||
|
||||
// Frame tail hook: only needed to restore light-view debug camera state before HUD.
|
||||
void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
restore_actual_light_debug();
|
||||
}
|
||||
|
||||
void add_control(UiElementHandle pane, const UiControlDesc& desc) {
|
||||
@@ -873,7 +1086,7 @@ ModResult build_controls_tab(
|
||||
"dynamic shadows. This can be expensive.");
|
||||
add_number(left, "Coverage", g_cvarBoxRadius, 1000, 20000, 500, nullptr,
|
||||
"Radius of the shadowed area around the camera, in world units. Smaller is sharper.");
|
||||
add_number(left, "Fade Out", g_cvarEdgeFadeWidth, 0, 128, 32, " texels",
|
||||
add_number(left, "Fade Out", g_cvarEdgeFadeWidth, 0, 256, 32, " texels",
|
||||
"Fade out shadows gradually near the edge of the coverage area.");
|
||||
|
||||
svc_ui->pane_add_section(mod_ctx, left, "Appearance");
|
||||
@@ -899,6 +1112,14 @@ ModResult build_controls_tab(
|
||||
"Bounds: valid X in red, valid Y in green, and valid depth in blue<br/>Light View: "
|
||||
"renders the game world directly from the light camera<br/>Camera Replay: "
|
||||
"captures the same draw-list replay from the gameplay camera");
|
||||
UiControlDesc debugWindowControl = UI_CONTROL_DESC_INIT;
|
||||
debugWindowControl.kind = UI_CONTROL_BUTTON;
|
||||
debugWindowControl.label = "Open / Close Debug Window";
|
||||
debugWindowControl.help_rml =
|
||||
"Shows the selected debug view in an auxiliary WebGPU window. Standard diagnostics leave "
|
||||
"the main view on the normal shadow composite.";
|
||||
debugWindowControl.on_pressed = on_toggle_debug_window;
|
||||
add_control(left, debugWindowControl);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -935,6 +1156,12 @@ ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
control.label = "Open Controls";
|
||||
control.on_pressed = on_open_controls;
|
||||
add_control(panel, control);
|
||||
|
||||
control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_BUTTON;
|
||||
control.label = "Open / Close Debug Window";
|
||||
control.on_pressed = on_toggle_debug_window;
|
||||
add_control(panel, control);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -1000,7 +1227,7 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
result = register_int_option("edgeFadeWidth", 32, g_cvarEdgeFadeWidth, error);
|
||||
result = register_int_option("edgeFadeWidth", 128, g_cvarEdgeFadeWidth, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
@@ -1041,6 +1268,12 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register stage hook");
|
||||
}
|
||||
stageDesc.callback = on_scene_after_opaque;
|
||||
if (svc_gfx->register_stage_hook(
|
||||
mod_ctx, GFX_STAGE_SCENE_AFTER_OPAQUE, &stageDesc, &g_sceneAfterOpaqueHook) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register stage hook");
|
||||
}
|
||||
stageDesc.callback = on_frame_before_hud;
|
||||
if (svc_gfx->register_stage_hook(
|
||||
mod_ctx, GFX_STAGE_FRAME_BEFORE_HUD, &stageDesc, &g_frameBeforeHudHook) != MOD_OK)
|
||||
@@ -1051,18 +1284,18 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
// Skip the game's own shadow rendering while the dynamic pass is active: the
|
||||
// shadowControl pair covers the actor real/blob shadows, drawCloudShadow the weather
|
||||
// cloud shadows.
|
||||
if (mods::hook_add_pre<GameShadowImageDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<GameShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<CloudShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK)
|
||||
if (mods::hook::add_pre<GameShadowImageDraw>(on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<GameShadowDraw>(on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<CloudShadowDraw>(on_game_shadow_pre) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering");
|
||||
}
|
||||
if (mods::hook_add_pre<ClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<ClipperBoxClip>(svc_hook, on_frustum_clip_pre) != MOD_OK)
|
||||
if (mods::hook::add_pre<ClipperSphereClip>(on_frustum_clip_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<ClipperBoxClip>(on_frustum_clip_pre) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping");
|
||||
}
|
||||
if (mods::hook_add_pre<CopyTex>(svc_hook, on_copy_tex_pre) != MOD_OK) {
|
||||
if (mods::hook::add_pre<CopyTex>(on_copy_tex_pre) != MOD_OK) {
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex");
|
||||
}
|
||||
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
|
||||
@@ -1078,6 +1311,8 @@ MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
restore_actual_light_debug();
|
||||
close_debug_window();
|
||||
release_debug_present_pipeline();
|
||||
svc_resource->free(mod_ctx, &g_shaderSource);
|
||||
if (g_compositePipeline != nullptr) {
|
||||
wgpuRenderPipelineRelease(g_compositePipeline);
|
||||
@@ -1099,8 +1334,11 @@ MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
g_cvarStrength = 0;
|
||||
g_cvarPcf = g_cvarBias = g_cvarBoxRadius = g_cvarEdgeFadeWidth = g_cvarContactShadows =
|
||||
g_cvarDebugView = 0;
|
||||
g_drawType = g_sceneBeginHook = g_sceneAfterTerrainHook = g_frameBeforeHudHook = 0;
|
||||
g_drawType = g_sceneBeginHook = g_sceneAfterTerrainHook = g_sceneAfterOpaqueHook =
|
||||
g_frameBeforeHudHook = 0;
|
||||
g_controlsWindow = 0;
|
||||
g_debugWindow = 0;
|
||||
g_debugPresentTarget = 0;
|
||||
g_mapPass = {};
|
||||
g_sceneCamera.valid = false;
|
||||
g_sceneCamera.raw_valid = false;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(window_demo 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(window_demo
|
||||
FEATURES fmt webgpu
|
||||
SOURCES src/logging.cpp src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
BUNDLE
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.window_demo",
|
||||
"name": "[Demo] Extra Window",
|
||||
"version": "1.0.0",
|
||||
"author": "Twilit Realm",
|
||||
"description": "Demonstrates creating an extra window through WindowService and rendering to it with GfxService."
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "logging.hpp"
|
||||
|
||||
#include "mods/svc/log.hpp"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char* window_event_name(WindowEventType type) {
|
||||
switch (type) {
|
||||
case WINDOW_EVENT_CLOSE_REQUESTED:
|
||||
return "close requested";
|
||||
case WINDOW_EVENT_RESIZED:
|
||||
return "resized";
|
||||
case WINDOW_EVENT_MOVED:
|
||||
return "moved";
|
||||
case WINDOW_EVENT_FOCUS_GAINED:
|
||||
return "focus gained";
|
||||
case WINDOW_EVENT_FOCUS_LOST:
|
||||
return "focus lost";
|
||||
case WINDOW_EVENT_SHOWN:
|
||||
return "shown";
|
||||
case WINDOW_EVENT_HIDDEN:
|
||||
return "hidden";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void window_demo::log_window_event(const WindowEvent* event) {
|
||||
mods::log::info(
|
||||
"window event: {}; position=({}, {}), size={}x{}, pixels={}x{}, scale={:.2f}",
|
||||
window_event_name(event->type), event->x, event->y, event->width, event->height,
|
||||
event->pixel_width, event->pixel_height, event->display_scale);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
struct WindowEvent;
|
||||
|
||||
namespace window_demo {
|
||||
|
||||
void log_window_event(const WindowEvent* event);
|
||||
|
||||
} // namespace window_demo
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "logging.hpp"
|
||||
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/gfx.h"
|
||||
#include "mods/svc/log.hpp"
|
||||
#include "mods/svc/ui.h"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
DEFINE_MOD();
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
IMPORT_SERVICE(UiService, svc_ui);
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
|
||||
namespace {
|
||||
|
||||
WindowHandle g_window = 0;
|
||||
GfxPresentTargetHandle g_presentTarget = 0;
|
||||
GfxStageHookHandle g_stageHook = 0;
|
||||
uint32_t g_frame = 0;
|
||||
bool g_recreatePresentTarget = false;
|
||||
|
||||
struct ClearPayload {
|
||||
float red;
|
||||
float green;
|
||||
float blue;
|
||||
float alpha;
|
||||
};
|
||||
static_assert(sizeof(ClearPayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE);
|
||||
|
||||
ModResult close_window() {
|
||||
g_recreatePresentTarget = false;
|
||||
if (g_presentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_presentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_presentTarget = 0;
|
||||
}
|
||||
if (g_window != 0) {
|
||||
const auto result = svc_window->destroy_window(mod_ctx, g_window);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_window = 0;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void on_window_event(ModContext*, WindowHandle, const WindowEvent* event, void*) {
|
||||
window_demo::log_window_event(event);
|
||||
|
||||
if (event->type == WINDOW_EVENT_CLOSE_REQUESTED) {
|
||||
if (close_window() != MOD_OK) {
|
||||
mods::log::error("failed to close auxiliary window");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render worker thread: record a clear of the acquired auxiliary surface texture.
|
||||
void on_present(
|
||||
ModContext*, const GfxPresentContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
if (payloadSize != sizeof(ClearPayload)) {
|
||||
return;
|
||||
}
|
||||
ClearPayload color;
|
||||
std::memcpy(&color, payload, sizeof(color));
|
||||
|
||||
WGPURenderPassColorAttachment colorAttachment = WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT;
|
||||
colorAttachment.view = ctx->target_view;
|
||||
colorAttachment.loadOp = WGPULoadOp_Clear;
|
||||
colorAttachment.storeOp = WGPUStoreOp_Store;
|
||||
colorAttachment.clearValue = WGPUColor{
|
||||
color.red,
|
||||
color.green,
|
||||
color.blue,
|
||||
color.alpha,
|
||||
};
|
||||
|
||||
WGPURenderPassDescriptor passDesc = WGPU_RENDER_PASS_DESCRIPTOR_INIT;
|
||||
passDesc.label = {"Auxiliary window clear", WGPU_STRLEN};
|
||||
passDesc.colorAttachmentCount = 1;
|
||||
passDesc.colorAttachments = &colorAttachment;
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(ctx->encoder, &passDesc);
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
ModResult register_present_target() {
|
||||
if (g_window == 0 || g_presentTarget != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
GfxPresentTargetDesc presentDesc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
presentDesc.label = "Auxiliary window surface";
|
||||
presentDesc.render = on_present;
|
||||
presentDesc.preferred_alpha_mode =
|
||||
WGPUCompositeAlphaMode_Premultiplied; // For transparent window
|
||||
return svc_gfx->register_window_present_target(
|
||||
mod_ctx, g_window, &presentDesc, &g_presentTarget);
|
||||
}
|
||||
|
||||
ModResult open_window() {
|
||||
if (g_window != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
WindowDesc windowDesc = WINDOW_DESC_INIT;
|
||||
windowDesc.title = "Mod window";
|
||||
windowDesc.width = 640;
|
||||
windowDesc.height = 480;
|
||||
windowDesc.on_event = on_window_event;
|
||||
windowDesc.flags |= WINDOW_FLAG_TRANSPARENT; // For transparent window
|
||||
auto result = svc_window->create_window(mod_ctx, &windowDesc, &g_window);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = register_present_target();
|
||||
if (result != MOD_OK) {
|
||||
close_window();
|
||||
return result;
|
||||
}
|
||||
|
||||
result = svc_window->show_window(mod_ctx, g_window);
|
||||
if (result != MOD_OK) {
|
||||
close_window();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void on_frame_after_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
if (g_presentTarget == 0) {
|
||||
return;
|
||||
}
|
||||
const float phase = static_cast<float>(g_frame++) * 0.015f;
|
||||
const ClearPayload color{
|
||||
.red = 0.08f + 0.06f * (std::sin(phase) + 1.0f),
|
||||
.green = 0.10f + 0.06f * (std::sin(phase + 2.1f) + 1.0f),
|
||||
.blue = 0.14f + 0.08f * (std::sin(phase + 4.2f) + 1.0f),
|
||||
.alpha = 0.5f,
|
||||
};
|
||||
if (svc_gfx->push_present(mod_ctx, g_presentTarget, &color, sizeof(color)) == MOD_ERROR) {
|
||||
g_recreatePresentTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
void on_toggle_window(ModContext*, void*) {
|
||||
if (g_window != 0) {
|
||||
if (close_window() != MOD_OK) {
|
||||
mods::log::error("failed to close auxiliary window");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (open_window() != MOD_OK) {
|
||||
mods::log::error("failed to open auxiliary window");
|
||||
}
|
||||
}
|
||||
|
||||
ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_BUTTON;
|
||||
control.label = "Open / Close Window";
|
||||
control.on_pressed = on_toggle_window;
|
||||
return svc_ui->pane_add_control(mod_ctx, panel, &control, nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT;
|
||||
stageDesc.callback = on_frame_after_hud;
|
||||
if (svc_gfx->register_stage_hook(
|
||||
mod_ctx, GFX_STAGE_FRAME_AFTER_HUD, &stageDesc, &g_stageHook) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register presentation hook");
|
||||
}
|
||||
|
||||
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
|
||||
panelDesc.build = build_panel;
|
||||
if (svc_ui->register_mods_panel(mod_ctx, &panelDesc) != MOD_OK) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register mod panel");
|
||||
}
|
||||
|
||||
if (open_window() != MOD_OK) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
return mods::set_error(error, MOD_ERROR, "failed to open auxiliary window");
|
||||
}
|
||||
|
||||
mods::log::info("auxiliary WebGPU window ready");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError* error) {
|
||||
if (!g_recreatePresentTarget || g_window == 0) {
|
||||
return MOD_OK;
|
||||
}
|
||||
g_recreatePresentTarget = false;
|
||||
if (g_presentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_presentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return mods::set_error(error, result, "failed to unregister lost present target");
|
||||
}
|
||||
g_presentTarget = 0;
|
||||
}
|
||||
const auto result = register_present_target();
|
||||
if (result != MOD_OK) {
|
||||
return mods::set_error(error, result, "failed to recreate present target");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
if (g_stageHook != 0) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
}
|
||||
close_window();
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
@@ -21,26 +21,9 @@ export JAVA_HOME="/usr/lib/jvm/java-17-openjdk"
|
||||
```bash
|
||||
cmake --preset android-arm64
|
||||
cmake --build --preset android-arm64
|
||||
|
||||
cmake --preset android-x86_64
|
||||
cmake --build --preset android-x86_64
|
||||
```
|
||||
|
||||
These builds produce:
|
||||
|
||||
- `build/android-arm64/Binaries/libmain.so`
|
||||
- `build/android-x86_64/Binaries/libmain.so`
|
||||
|
||||
## Stage Libraries Into APK Project
|
||||
|
||||
```bash
|
||||
./android/scripts/stage-jni-libs.sh
|
||||
```
|
||||
|
||||
This copies:
|
||||
|
||||
- `libmain.so` -> `android/app/src/main/jniLibs/arm64-v8a/`
|
||||
- `libmain.so` -> `android/app/src/main/jniLibs/x86_64/`
|
||||
This build produces `build/android-arm64/libmain.so`
|
||||
|
||||
## Refresh SDL Java Shim (Optional)
|
||||
|
||||
|
||||
@@ -6,23 +6,107 @@ def versionNameStr = (System.getenv("DUSK_VERSION") ?: "v0.1.0").replaceFirst("^
|
||||
def versionCodeInt = (System.getenv("DUSK_VERSION_CODE") ?: "100000").toInteger()
|
||||
|
||||
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
|
||||
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight').get().asFile
|
||||
def androidNativeBuildDir = new File(duskRepoDir, 'build/android-arm64')
|
||||
def nativeLibrary = new File(androidNativeBuildDir, 'libmain.so')
|
||||
def stageStripValue = providers.gradleProperty('ANDROID_STAGE_STRIP')
|
||||
.orElse(providers.gradleProperty('androidStageStrip'))
|
||||
.orElse(providers.environmentVariable('ANDROID_STAGE_STRIP'))
|
||||
.orElse('1')
|
||||
def androidHome = {
|
||||
def sdkPath = System.getenv('ANDROID_HOME')
|
||||
if (!sdkPath) {
|
||||
throw new GradleException('ANDROID_HOME is not available')
|
||||
}
|
||||
def sdkDir = new File(sdkPath)
|
||||
if (!sdkDir.isDirectory()) {
|
||||
throw new GradleException("ANDROID_HOME points to a missing directory: ${sdkDir}")
|
||||
}
|
||||
sdkDir
|
||||
}.memoize()
|
||||
|
||||
def androidNdkVersion = {
|
||||
def ndkVersion = System.getenv('ANDROID_NDK_VERSION')
|
||||
if (!ndkVersion) {
|
||||
throw new GradleException('ANDROID_NDK_VERSION is not available')
|
||||
}
|
||||
ndkVersion
|
||||
}.memoize()
|
||||
|
||||
def androidNdkDir = {
|
||||
def ndkDir = new File(androidHome(), "ndk/${androidNdkVersion()}")
|
||||
if (!new File(ndkDir, 'build/cmake/android.toolchain.cmake').isFile()) {
|
||||
throw new GradleException(
|
||||
"Android NDK ${androidNdkVersion()} is missing or invalid at ${ndkDir}"
|
||||
)
|
||||
}
|
||||
ndkDir
|
||||
}.memoize()
|
||||
|
||||
def llvmPrebuiltDir = {
|
||||
def prebuiltRoot = new File(androidNdkDir(), 'toolchains/llvm/prebuilt')
|
||||
def prebuiltDir = (prebuiltRoot.listFiles()?.findAll { it.isDirectory() } ?: [])
|
||||
.sort { it.name }
|
||||
.find { candidate ->
|
||||
new File(
|
||||
candidate,
|
||||
'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so'
|
||||
).isFile()
|
||||
}
|
||||
if (!prebuiltDir) {
|
||||
throw new GradleException("Cannot find NDK libc++_shared.so under ${prebuiltRoot}")
|
||||
}
|
||||
prebuiltDir
|
||||
}.memoize()
|
||||
|
||||
def stlLibrary = {
|
||||
def library = new File(
|
||||
llvmPrebuiltDir(),
|
||||
'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so'
|
||||
)
|
||||
if (!library.isFile()) {
|
||||
throw new GradleException("libc++_shared.so is missing")
|
||||
}
|
||||
library
|
||||
}.memoize()
|
||||
|
||||
def stagedJniLibsDir = layout.buildDirectory.dir('generated/jniLibs/dusklight')
|
||||
|
||||
def stageJniLibs = tasks.register('stageJniLibs', Sync) {
|
||||
group = 'build'
|
||||
from(nativeLibrary) {
|
||||
rename { 'libmain.so' }
|
||||
into 'arm64-v8a'
|
||||
}
|
||||
from(providers.provider { stlLibrary() }) {
|
||||
rename { 'libc++_shared.so' }
|
||||
into 'arm64-v8a'
|
||||
}
|
||||
into(stagedJniLibsDir)
|
||||
|
||||
doFirst {
|
||||
if (!nativeLibrary.isFile()) {
|
||||
throw new GradleException("Native library is missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight')
|
||||
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
|
||||
from(new File(duskRepoDir, 'res')) {
|
||||
into 'res'
|
||||
exclude '**/.DS_Store'
|
||||
}
|
||||
// Staged by platforms/android/scripts/stage-jni-libs.sh
|
||||
from(new File(projectDir, 'src/main/bundled_mods')) {
|
||||
from(new File(androidNativeBuildDir, 'bundled_mods')) {
|
||||
into 'mods'
|
||||
include '*.dusk'
|
||||
}
|
||||
into duskGeneratedAssetsDir
|
||||
into(duskGeneratedAssetsDir)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'dev.twilitrealm.dusk'
|
||||
compileSdk 36
|
||||
ndkVersion androidNdkVersion()
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'dev.twilitrealm.dusk'
|
||||
@@ -44,16 +128,24 @@ android {
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||
jniLibs.srcDirs = [stagedJniLibsDir]
|
||||
assets.srcDirs = [duskGeneratedAssetsDir]
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
if (stageStripValue.get() == '0') {
|
||||
keepDebugSymbols += '**/libmain.so'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'arm64-v8a', 'x86_64'
|
||||
include 'arm64-v8a'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
@@ -67,9 +159,6 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
tasks.configureEach { task ->
|
||||
if ((task.name.startsWith('merge') && task.name.endsWith('Assets')) ||
|
||||
task.name.toLowerCase().contains('lint')) {
|
||||
task.dependsOn(syncDuskAssets)
|
||||
}
|
||||
tasks.named('preBuild').configure {
|
||||
dependsOn(stageJniLibs, syncDuskAssets)
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
APP_DIR="$ROOT_DIR/platforms/android/app/src/main/jniLibs"
|
||||
ANDROID_HOME_DIR="${ANDROID_HOME:-$HOME/Android/Sdk}"
|
||||
ANDROID_NDK_VER="${ANDROID_NDK_VERSION:-}"
|
||||
ANDROID_STAGE_ABIS="${ANDROID_STAGE_ABIS:-arm64-v8a x86_64}"
|
||||
ANDROID_STAGE_STRIP="${ANDROID_STAGE_STRIP:-1}"
|
||||
STRIP_TOOL=""
|
||||
|
||||
if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then
|
||||
ANDROID_NDK_VER="$(ls -1 "$ANDROID_HOME_DIR/ndk" | sort -V | tail -n 1)"
|
||||
fi
|
||||
|
||||
if [[ -n "$ANDROID_NDK_VER" ]]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin) HOST_TAG="darwin-x86_64" ;;
|
||||
Linux) HOST_TAG="linux-x86_64" ;;
|
||||
*) HOST_TAG="" ;;
|
||||
esac
|
||||
|
||||
PREBUILT_DIR="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt"
|
||||
if [[ -n "$HOST_TAG" && -x "$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" ]]; then
|
||||
STRIP_TOOL="$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip"
|
||||
else
|
||||
for candidate in "$PREBUILT_DIR"/*/bin/llvm-strip; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
STRIP_TOOL="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
copy_lib() {
|
||||
local abi="$1"
|
||||
local src="$2"
|
||||
local dst_dir="$APP_DIR/$abi"
|
||||
local dst="$dst_dir/libmain.so"
|
||||
local tmp="$dst_dir/.libmain.so.$$"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
echo "Missing native library for $abi: $src" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$dst_dir"
|
||||
cp -f "$src" "$tmp"
|
||||
if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then
|
||||
"$STRIP_TOOL" --strip-unneeded "$tmp"
|
||||
mv -f "$tmp" "$dst"
|
||||
echo "Stripped and staged $src -> $dst"
|
||||
else
|
||||
mv -f "$tmp" "$dst"
|
||||
echo "Staged $src -> $dst (strip disabled or strip tool unavailable)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Drop any previously staged ABI directories to avoid stale APK contents.
|
||||
rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64"
|
||||
|
||||
for abi in $ANDROID_STAGE_ABIS; do
|
||||
case "$abi" in
|
||||
arm64-v8a)
|
||||
src="$ROOT_DIR/build/android-arm64/libmain.so"
|
||||
triple="aarch64-linux-android"
|
||||
;;
|
||||
x86_64)
|
||||
src="$ROOT_DIR/build/android-x86_64/libmain.so"
|
||||
triple="x86_64-linux-android"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
copy_lib "$abi" "$src"
|
||||
if [[ -n "$STRIP_TOOL" ]]; then
|
||||
stl="$(dirname "$STRIP_TOOL")/../sysroot/usr/lib/$triple/libc++_shared.so"
|
||||
if [[ -f "$stl" ]]; then
|
||||
cp -f "$stl" "$APP_DIR/$abi/libc++_shared.so"
|
||||
echo "Staged $stl -> $APP_DIR/$abi/libc++_shared.so"
|
||||
else
|
||||
echo "Missing libc++_shared.so for $abi at $stl" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Cannot stage libc++_shared.so for $abi (NDK not found)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Stage bundled mod packages into the app's assets source dir.
|
||||
MODS_STAGING_DIR="$ROOT_DIR/platforms/android/app/src/main/bundled_mods"
|
||||
rm -rf "$MODS_STAGING_DIR"
|
||||
mkdir -p "$MODS_STAGING_DIR"
|
||||
for abi in $ANDROID_STAGE_ABIS; do
|
||||
case "$abi" in
|
||||
arm64-v8a) build_dir="$ROOT_DIR/build/android-arm64" ;;
|
||||
x86_64) build_dir="$ROOT_DIR/build/android-x86_64" ;;
|
||||
esac
|
||||
[[ -d "$build_dir/bundled_mods" ]] || continue
|
||||
for pkg in "$build_dir/bundled_mods"/*.dusk; do
|
||||
[[ -f "$pkg" ]] || continue
|
||||
name="$(basename "$pkg")"
|
||||
if [[ ! -f "$MODS_STAGING_DIR/$name" ]]; then
|
||||
cp -f "$pkg" "$MODS_STAGING_DIR/$name"
|
||||
echo "Staged bundled mod $pkg"
|
||||
else
|
||||
stage_dir="$build_dir/mods/${name%.dusk}/${name%.dusk}_stage"
|
||||
(cd "$stage_dir" && zip -q -r "$MODS_STAGING_DIR/$name" lib)
|
||||
echo "Appended $abi libraries to bundled mod $name"
|
||||
fi
|
||||
done
|
||||
done
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
#
|
||||
# Usage (from a mod project):
|
||||
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
|
||||
# add_mod(my_mod FEATURES game webgpu SOURCES ... MOD_JSON mod.json)
|
||||
# add_mod(my_mod FEATURES fmt game webgpu SOURCES ... MOD_JSON mod.json)
|
||||
#
|
||||
# On platforms where mods link against the game binary (Windows/Apple/Android), a
|
||||
# version-independent link stub is downloaded automatically unless DUSK_GAME_EXE is set.
|
||||
|
||||
+17
-1
@@ -20,7 +20,23 @@ extern "C" {
|
||||
#ifdef __cplusplus
|
||||
#define MOD_EXTERN_C extern "C"
|
||||
#else
|
||||
#define MOD_EXTERN_C
|
||||
#define MOD_EXTERN_C extern
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define MOD_DECLARE_SERVICE( \
|
||||
service_type, variable, service_id_value, major_value, minor_value) \
|
||||
MOD_EXTERN_C const service_type* variable; \
|
||||
template <> \
|
||||
struct mods::ServiceTraits<service_type> { \
|
||||
static constexpr const char* id = service_id_value; \
|
||||
static constexpr uint16_t major_version = major_value; \
|
||||
static constexpr uint16_t minor_version = minor_value; \
|
||||
}
|
||||
#else
|
||||
#define MOD_DECLARE_SERVICE( \
|
||||
service_type, variable, service_id_value, major_value, minor_value) \
|
||||
MOD_EXTERN_C const service_type* variable
|
||||
#endif
|
||||
|
||||
#define MOD_ABI_VERSION 1u
|
||||
|
||||
+25
-179
@@ -1,210 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_GAME)
|
||||
#error "DEFINE_HOOK requires add_mod(... FEATURES game)"
|
||||
#if defined(_MSC_VER)
|
||||
#pragma message("warning: <mods/hook.hpp> is deprecated; include <mods/svc/hook.hpp> instead")
|
||||
#else
|
||||
#warning "<mods/hook.hpp> is deprecated; include <mods/svc/hook.hpp> instead"
|
||||
#endif
|
||||
|
||||
#include <mods/svc/hook.h>
|
||||
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <mods/svc/hook.hpp>
|
||||
|
||||
namespace mods {
|
||||
|
||||
template <class T>
|
||||
T arg(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Trampoline generator + per-target state. Tag makes each hooked target's statics distinct; the
|
||||
* target address comes from the declaration's metadata record, resolved by the host at mod
|
||||
* initialization.
|
||||
*/
|
||||
template <class Tag, class R, class... A>
|
||||
struct HookImpl {
|
||||
static inline R (*g_orig)(A...) = nullptr;
|
||||
static inline const HookService* hooks = nullptr;
|
||||
static inline void* target = nullptr;
|
||||
|
||||
static bool dispatch_pre(void* args, void* retval) {
|
||||
if (hooks == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int skipOriginal = 0;
|
||||
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
|
||||
return result == MOD_OK && skipOriginal != 0;
|
||||
}
|
||||
|
||||
static void dispatch_post(void* args, void* retval) {
|
||||
if (hooks != nullptr) {
|
||||
hooks->dispatch_post(mod_ctx, target, args, retval);
|
||||
}
|
||||
}
|
||||
|
||||
static R trampoline(A... args) {
|
||||
if constexpr (sizeof...(A) == 0) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal =
|
||||
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal = dispatch_pre(
|
||||
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <auto Target>
|
||||
using TargetTag = std::integral_constant<decltype(Target), Target>;
|
||||
template <FixedString Name>
|
||||
struct NameTag {};
|
||||
} // namespace detail
|
||||
|
||||
/*
|
||||
* Typed base for a hook on a function named at compile time (&daAlink_c::execute, &free_fn).
|
||||
* Instantiate through DEFINE_HOOK, which pairs it with the metadata record the host resolves.
|
||||
*/
|
||||
template <auto Target>
|
||||
struct Hook;
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {};
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...) const>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {};
|
||||
|
||||
template <class R, class... A, R (*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {};
|
||||
|
||||
/*
|
||||
* Typed base for a hook on a function by its symbol name, for targets you can't name in C++:
|
||||
* file-local statics, private members, or symbols without a header. The signature is written
|
||||
* free-style with the receiver first and is *not* compiler-checked. Instantiate through
|
||||
* DEFINE_HOOK_SYMBOL.
|
||||
*/
|
||||
template <FixedString Name, class Sig>
|
||||
struct NamedHook;
|
||||
|
||||
template <FixedString Name, class R, class... A>
|
||||
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, R, A...> {};
|
||||
|
||||
/*
|
||||
* Declare a hook target. The declaration emits a metadata record that the host resolves at mod
|
||||
* initialization. Every hook target must be declared.
|
||||
*
|
||||
* DEFINE_HOOK(&daAlink_c::execute, LinkExecute);
|
||||
* DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
|
||||
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
|
||||
*
|
||||
* mods::hook_add_pre<LinkExecute>(svc_hook, on_link_execute);
|
||||
*
|
||||
* DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O
|
||||
* leading underscore) or the demangled qualified display name; overloaded display names are
|
||||
* ambiguous and need the mangled form.
|
||||
*/
|
||||
#define DEFINE_HOOK(target, alias) \
|
||||
[[maybe_unused]] static const void* const mod_meta_hook_##alias = \
|
||||
&::mods::detail::HookRecordFor<(target), ::mods::FixedString{#target}>::Holder::record; \
|
||||
struct alias : ::mods::Hook<(target)> { \
|
||||
static void* resolved_target() { \
|
||||
return ::mods::detail::HookRecordFor<(target), \
|
||||
::mods::FixedString{#target}>::Holder::record.resolved; \
|
||||
} \
|
||||
}
|
||||
|
||||
#define DEFINE_HOOK_SYMBOL(name, sig, alias) \
|
||||
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
|
||||
::mods::detail::make_hook_name_record<::mods::FixedString{name}>(); \
|
||||
struct alias : ::mods::NamedHook<::mods::FixedString{name}, sig> { \
|
||||
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
if (hooks == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return hook::install<Entry>(hooks);
|
||||
}
|
||||
|
||||
Entry::hooks = hooks;
|
||||
if (Entry::target == nullptr) {
|
||||
void* resolved = Entry::resolved_target();
|
||||
if (resolved == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
Entry::target = resolved;
|
||||
}
|
||||
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
|
||||
reinterpret_cast<void**>(&Entry::g_orig));
|
||||
template <class Entry>
|
||||
ModResult hook_install() {
|
||||
return hook::install<Entry>();
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::add_pre<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_add_pre(HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::add_pre<Entry>(callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::add_post<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->add_post(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_add_post(HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::add_post<Entry>(callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::replace<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->replace(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_replace(HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::replace<Entry>(callback, options);
|
||||
}
|
||||
|
||||
} // namespace mods
|
||||
|
||||
@@ -241,6 +241,48 @@ consteval auto make_hook_mem_names() {
|
||||
return r;
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) && !defined(__clang__) && defined(__ELF__)
|
||||
/* https://gcc.gnu.org/bugzilla/show_bug.cgi?id=41091 prevents inline static template members from
|
||||
* sharing an explicit ELF section with ordinary variables. GCC can instead constant-evaluate a
|
||||
* file-local record at each DEFINE_HOOK. */
|
||||
template <auto Target>
|
||||
void materialize_hook_mem(unsigned char* outPmf) {
|
||||
const auto target = Target;
|
||||
std::memcpy(outPmf, &target, sizeof(target));
|
||||
}
|
||||
|
||||
template <auto Target, FixedString Disp>
|
||||
consteval auto make_local_hook_record() {
|
||||
using F = decltype(Target);
|
||||
if constexpr (std::is_member_function_pointer_v<F>) {
|
||||
constexpr auto names = make_hook_mem_names<Target, Disp>();
|
||||
static_assert(sizeof(F) <= MOD_META_HOOK_MEM_EXT_CAPACITY,
|
||||
"unsupported pointer-to-member representation");
|
||||
if constexpr (sizeof(F) > MOD_META_HOOK_MEM_CAPACITY) {
|
||||
HookMemExtRecord<names.len> record = {
|
||||
{sizeof(HookMemExtRecord<names.len>), MOD_META_HOOK_MEM_EXT, 0}, sizeof(F),
|
||||
materialize_hook_mem<Target>, nullptr, {}};
|
||||
for (size_t i = 0; i < names.len; ++i) {
|
||||
record.names[i] = names.chars[i];
|
||||
}
|
||||
return record;
|
||||
} else {
|
||||
HookMemRecord<F, names.len> record = {
|
||||
{sizeof(HookMemRecord<F, names.len>), MOD_META_HOOK_MEM, 0}, 0, {Target}, nullptr,
|
||||
{}};
|
||||
for (size_t i = 0; i < names.len; ++i) {
|
||||
record.names[i] = names.chars[i];
|
||||
}
|
||||
return record;
|
||||
}
|
||||
} else {
|
||||
static_assert(std::is_pointer_v<F> && std::is_function_v<std::remove_pointer_t<F>>,
|
||||
"hook target must be a function or member function");
|
||||
return HookFnRecord<F>{{sizeof(HookFnRecord<F>), MOD_META_HOOK_FN, 0}, 0, Target, nullptr};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* MSVC constant-evaluates a compact pointer-to-member only when every other operand in the
|
||||
* initializer is a literal: no consteval calls, constexpr-object copies, or default member
|
||||
|
||||
@@ -40,13 +40,13 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa
|
||||
}; \
|
||||
}
|
||||
|
||||
// Declares `static const service_type* variable`, filled in by the host before mod_initialize.
|
||||
// Defines `const service_type* variable`, filled in by the host before mod_initialize.
|
||||
// Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports
|
||||
// must be checked against nullptr before use. The unversioned macros use the latest minor version;
|
||||
// set an explicit version to target an older minor version for backwards compatibility.
|
||||
#define IMPORT_SERVICE_EX( \
|
||||
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
|
||||
static const service_type* variable = nullptr; \
|
||||
const service_type* variable = nullptr; \
|
||||
MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = { \
|
||||
{sizeof(ModMetaImport), MOD_META_IMPORT, static_cast<uint8_t>(flags_value)}, \
|
||||
static_cast<uint16_t>(major_value), \
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera"
|
||||
#define CAMERA_SERVICE_MAJOR 1u
|
||||
#define CAMERA_SERVICE_MINOR 0u
|
||||
@@ -53,13 +57,5 @@ typedef struct CameraService {
|
||||
ModResult (*get_camera)(ModContext* ctx, const void* game_view, CameraInfo* out_info);
|
||||
} CameraService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<CameraService> {
|
||||
static constexpr const char* id = CAMERA_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CAMERA_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = CAMERA_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(
|
||||
CameraService, svc_camera, CAMERA_SERVICE_ID, CAMERA_SERVICE_MAJOR, CAMERA_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config"
|
||||
#define CONFIG_SERVICE_MAJOR 1u
|
||||
#define CONFIG_SERVICE_MINOR 0u
|
||||
@@ -96,13 +100,5 @@ typedef struct ConfigService {
|
||||
ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle);
|
||||
} ConfigService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<ConfigService> {
|
||||
static constexpr const char* id = CONFIG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = CONFIG_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(
|
||||
ConfigService, svc_config, CONFIG_SERVICE_ID, CONFIG_SERVICE_MAJOR, CONFIG_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* The mod SDK imports this service automatically for mods built with FEATURES game; service-only
|
||||
* and asset-only mods do not require it.
|
||||
@@ -19,13 +23,4 @@ typedef struct GameService {
|
||||
ServiceHeader header;
|
||||
} GameService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<GameService> {
|
||||
static constexpr const char* id = GAME_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = GAME_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(GameService, svc_game, GAME_SERVICE_ID, GAME_SERVICE_MAJOR, GAME_SERVICE_MINOR);
|
||||
|
||||
+72
-12
@@ -1,6 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
#include <mods/svc/window.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_WEBGPU)
|
||||
#error "mods/svc/gfx.h requires add_mod(... FEATURES webgpu)"
|
||||
@@ -28,7 +33,7 @@
|
||||
|
||||
#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx"
|
||||
#define GFX_SERVICE_MAJOR 1u
|
||||
#define GFX_SERVICE_MINOR 0u
|
||||
#define GFX_SERVICE_MINOR 1u
|
||||
|
||||
/* Maximum size for push_draw payload */
|
||||
#define GFX_INLINE_DRAW_PAYLOAD_SIZE 128u
|
||||
@@ -37,6 +42,7 @@
|
||||
typedef uint64_t GfxDrawTypeHandle;
|
||||
typedef uint64_t GfxStageHookHandle;
|
||||
typedef uint64_t GfxComputeTypeHandle;
|
||||
typedef uint64_t GfxPresentTargetHandle;
|
||||
|
||||
/* A suballocation in one of the shared per-frame streaming buffers. */
|
||||
typedef struct GfxRange {
|
||||
@@ -56,11 +62,13 @@ typedef struct GfxDeviceInfo {
|
||||
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 */
|
||||
WGPUInstance instance; /* borrowed; added in GfxService 1.1 */
|
||||
WGPUAdapter adapter; /* borrowed; added in GfxService 1.1 */
|
||||
} GfxDeviceInfo;
|
||||
|
||||
#define GFX_DEVICE_INFO_INIT \
|
||||
{sizeof(GfxDeviceInfo), NULL, NULL, WGPUTextureFormat_Undefined, WGPUTextureFormat_Undefined, \
|
||||
1u, false}
|
||||
1u, false, NULL, NULL}
|
||||
|
||||
/*
|
||||
* Passed to GfxDrawFn on the render worker thread; valid only during the call. The pass pipeline,
|
||||
@@ -168,6 +176,48 @@ typedef struct GfxComputeTypeDesc {
|
||||
|
||||
#define GFX_COMPUTE_TYPE_DESC_INIT {sizeof(GfxComputeTypeDesc), NULL, NULL, NULL}
|
||||
|
||||
/*
|
||||
* Invoked on the render worker while the frame encoder is open. The target texture and view have
|
||||
* been acquired by the host and are borrowed for the callback. Record all target work on encoder,
|
||||
* leave no pass open, and do not finish, submit, or present it. The host submits the shared command
|
||||
* buffer and presents the target after submission. The streaming buffers contain data appended on
|
||||
* the game thread before push_present.
|
||||
*/
|
||||
typedef struct GfxPresentContext {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device;
|
||||
WGPUQueue queue;
|
||||
WGPUCommandEncoder encoder;
|
||||
WGPUTexture target_texture;
|
||||
WGPUTextureView target_view;
|
||||
WGPUTextureFormat target_format;
|
||||
uint32_t target_width;
|
||||
uint32_t target_height;
|
||||
WGPUBuffer vertex_buffer;
|
||||
WGPUBuffer index_buffer;
|
||||
WGPUBuffer uniform_buffer;
|
||||
WGPUBuffer storage_buffer;
|
||||
} GfxPresentContext;
|
||||
|
||||
typedef void (*GfxPresentFn)(ModContext* ctx, const GfxPresentContext* present_ctx,
|
||||
const void* payload, size_t payload_size, void* user_data);
|
||||
|
||||
typedef struct GfxPresentTargetDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* optional debug label */
|
||||
uint32_t width; /* required for raw surfaces; ignored for WindowService windows */
|
||||
uint32_t height;
|
||||
WGPUTextureUsage usage; /* 0 defaults to RenderAttachment */
|
||||
WGPUTextureFormat preferred_format;
|
||||
WGPUCompositeAlphaMode preferred_alpha_mode;
|
||||
GfxPresentFn render;
|
||||
void* user_data;
|
||||
} GfxPresentTargetDesc;
|
||||
|
||||
#define GFX_PRESENT_TARGET_DESC_INIT \
|
||||
{sizeof(GfxPresentTargetDesc), NULL, 0u, 0u, WGPUTextureUsage_None, \
|
||||
WGPUTextureFormat_Undefined, WGPUCompositeAlphaMode_Auto, NULL, NULL}
|
||||
|
||||
typedef struct GfxService {
|
||||
ServiceHeader header;
|
||||
|
||||
@@ -200,15 +250,25 @@ typedef struct GfxService {
|
||||
ModResult (*resolve_pass)(
|
||||
ModContext* ctx, const GfxResolveDesc* desc, GfxResolvedTargets* out_targets);
|
||||
ModResult (*create_pass)(ModContext* ctx, uint32_t width, uint32_t height);
|
||||
|
||||
/* Minor version 1 */
|
||||
|
||||
ModResult (*register_present_target)(ModContext* ctx, WGPUSurface surface,
|
||||
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* out_handle);
|
||||
ModResult (*register_window_present_target)(ModContext* ctx, WindowHandle window,
|
||||
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* out_handle);
|
||||
/* Raw-surface targets only; WindowService target resizes are managed automatically. */
|
||||
ModResult (*resize_present_target)(
|
||||
ModContext* ctx, GfxPresentTargetHandle handle, uint32_t width, uint32_t height);
|
||||
ModResult (*unregister_present_target)(ModContext* ctx, GfxPresentTargetHandle handle);
|
||||
/*
|
||||
* MOD_OK means the task was queued.
|
||||
* MOD_UNAVAILABLE means no task could be queued now (for example, a window has no pixel size).
|
||||
* MOD_ERROR means an earlier task found the surface lost or deterministically invalid;
|
||||
* unregister and recreate the target before pushing again.
|
||||
*/
|
||||
ModResult (*push_present)(
|
||||
ModContext* ctx, GfxPresentTargetHandle handle, const void* payload, size_t payload_size);
|
||||
} GfxService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<GfxService> {
|
||||
static constexpr const char* id = GFX_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GFX_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = GFX_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(GfxService, svc_gfx, GFX_SERVICE_ID, GFX_SERVICE_MAJOR, GFX_SERVICE_MINOR);
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Intercept game functions by address. Prefer the typed helpers in mods/hook.hpp
|
||||
* (hook_add_pre/hook_add_post/hook_replace over a &Class::method): they generate the
|
||||
* Intercept game functions by address. Prefer the typed helpers in mods/svc/hook.hpp
|
||||
* (mods::hook::add_pre/add_post/replace over a &Class::method): they generate the
|
||||
* trampoline and hide install/dispatch, which are the low-level primitives those helpers
|
||||
* build. resolve() maps a symbol name to an address for targets you can't name at compile time
|
||||
* (file-local statics included).
|
||||
@@ -46,7 +50,7 @@ typedef enum HookReplacePolicy {
|
||||
/*
|
||||
* Hook callbacks. `args` is an array of pointers to the call's arguments (index 0 is `this`
|
||||
* for member functions); `retval` points at the return slot (NULL for void). Read and write
|
||||
* them through mods::arg<T> / arg_ref<T> from mods/hook.hpp. `userdata` is the pointer
|
||||
* them through mods::arg<T> / arg_ref<T> from mods/svc/hook.hpp. `userdata` is the pointer
|
||||
* from HookOptions. All run on the game thread, in the hooked call's own stack frame.
|
||||
*/
|
||||
typedef HookAction (*HookPreFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
@@ -114,13 +118,4 @@ typedef struct HookService {
|
||||
ModContext* ctx, const char* symbol, void** out_addr, HookSymbolFlags* out_flags);
|
||||
} HookService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<HookService> {
|
||||
static constexpr const char* id = HOOK_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = HOOK_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(HookService, svc_hook, HOOK_SERVICE_ID, HOOK_SERVICE_MAJOR, HOOK_SERVICE_MINOR);
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
#pragma once
|
||||
|
||||
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_GAME)
|
||||
#error "DEFINE_HOOK requires add_mod(... FEATURES game)"
|
||||
#endif
|
||||
|
||||
#include <mods/svc/hook.h>
|
||||
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
namespace mods {
|
||||
|
||||
template <class T>
|
||||
T arg(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Trampoline generator + per-target state. Tag makes each hooked target's statics distinct; the
|
||||
* target address comes from the declaration's metadata record, resolved by the host at mod
|
||||
* initialization.
|
||||
*/
|
||||
template <class Tag, class R, class... A>
|
||||
struct HookImpl {
|
||||
static inline R (*g_orig)(A...) = nullptr;
|
||||
static inline const HookService* hooks = nullptr;
|
||||
static inline void* target = nullptr;
|
||||
|
||||
static bool dispatch_pre(void* args, void* retval) {
|
||||
if (hooks == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int skipOriginal = 0;
|
||||
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
|
||||
return result == MOD_OK && skipOriginal != 0;
|
||||
}
|
||||
|
||||
static void dispatch_post(void* args, void* retval) {
|
||||
if (hooks != nullptr) {
|
||||
hooks->dispatch_post(mod_ctx, target, args, retval);
|
||||
}
|
||||
}
|
||||
|
||||
static R trampoline(A... args) {
|
||||
if constexpr (sizeof...(A) == 0) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal =
|
||||
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal = dispatch_pre(
|
||||
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <auto Target>
|
||||
using TargetTag = std::integral_constant<decltype(Target), Target>;
|
||||
template <FixedString Name>
|
||||
struct NameTag {};
|
||||
} // namespace detail
|
||||
|
||||
/*
|
||||
* Typed base for a hook on a function named at compile time (&daAlink_c::execute, &free_fn).
|
||||
* Instantiate through DEFINE_HOOK, which pairs it with the metadata record the host resolves.
|
||||
*/
|
||||
template <auto Target>
|
||||
struct Hook;
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {};
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...) const>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {};
|
||||
|
||||
template <class R, class... A, R (*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {};
|
||||
|
||||
/*
|
||||
* Typed base for a hook on a function by its symbol name, for targets you can't name in C++:
|
||||
* file-local statics, private members, or symbols without a header. The signature is written
|
||||
* free-style with the receiver first and is *not* compiler-checked. Instantiate through
|
||||
* DEFINE_HOOK_SYMBOL.
|
||||
*/
|
||||
template <FixedString Name, class Sig>
|
||||
struct NamedHook;
|
||||
|
||||
template <FixedString Name, class R, class... A>
|
||||
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, R, A...> {};
|
||||
|
||||
/*
|
||||
* Declare a hook target. The declaration emits a metadata record that the host resolves at mod
|
||||
* initialization. Every hook target must be declared.
|
||||
*
|
||||
* DEFINE_HOOK(&daAlink_c::execute, LinkExecute);
|
||||
* DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
|
||||
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
|
||||
*
|
||||
* mods::hook::add_pre<LinkExecute>(on_link_execute);
|
||||
*
|
||||
* DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O
|
||||
* leading underscore) or the demangled qualified display name; overloaded display names are
|
||||
* ambiguous and need the mangled form.
|
||||
*/
|
||||
#if defined(__GNUC__) && !defined(__clang__) && defined(__ELF__)
|
||||
#define DEFINE_HOOK(target, alias) \
|
||||
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
|
||||
::mods::detail::make_local_hook_record<(target), ::mods::FixedString{#target}>(); \
|
||||
struct alias : ::mods::Hook<(target)> { \
|
||||
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
|
||||
}
|
||||
#else
|
||||
#define DEFINE_HOOK(target, alias) \
|
||||
[[maybe_unused]] static const void* const mod_meta_hook_##alias = \
|
||||
&::mods::detail::HookRecordFor<(target), ::mods::FixedString{#target}>::Holder::record; \
|
||||
struct alias : ::mods::Hook<(target)> { \
|
||||
static void* resolved_target() { \
|
||||
return ::mods::detail::HookRecordFor<(target), \
|
||||
::mods::FixedString{#target}>::Holder::record.resolved; \
|
||||
} \
|
||||
}
|
||||
#endif
|
||||
|
||||
#define DEFINE_HOOK_SYMBOL(name, sig, alias) \
|
||||
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
|
||||
::mods::detail::make_hook_name_record<::mods::FixedString{name}>(); \
|
||||
struct alias : ::mods::NamedHook<::mods::FixedString{name}, sig> { \
|
||||
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
|
||||
}
|
||||
|
||||
namespace hook {
|
||||
|
||||
template <class Entry>
|
||||
ModResult install(const HookService* hooks) {
|
||||
if (hooks == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
Entry::hooks = hooks;
|
||||
if (Entry::target == nullptr) {
|
||||
void* resolved = Entry::resolved_target();
|
||||
if (resolved == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
Entry::target = resolved;
|
||||
}
|
||||
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
|
||||
reinterpret_cast<void**>(&Entry::g_orig));
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult install() {
|
||||
return install<Entry>(svc_hook);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult add_pre(HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
return add_pre<Entry>(svc_hook, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
return hooks->add_post(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult add_post(HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
return add_post<Entry>(svc_hook, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
return hooks->replace(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult replace(HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
return replace<Entry>(svc_hook, callback, options);
|
||||
}
|
||||
|
||||
} // namespace hook
|
||||
} // namespace mods
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* The host service: the calling mod's identity and its runtime interface to the loader.
|
||||
* Always available; every other service can be reached from it.
|
||||
@@ -103,13 +107,4 @@ typedef struct HostService {
|
||||
const char* (*native_dir)(ModContext* ctx);
|
||||
} HostService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<HostService> {
|
||||
static constexpr const char* id = HOST_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOST_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = HOST_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(HostService, svc_host, HOST_SERVICE_ID, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Logging into the game's console and log files. Messages are attributed to the calling mod
|
||||
* (prefixed with its ID).
|
||||
@@ -36,13 +40,4 @@ typedef struct LogService {
|
||||
void (*error)(ModContext* ctx, const char* message);
|
||||
} LogService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<LogService> {
|
||||
static constexpr const char* id = LOG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = LOG_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = LOG_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(LogService, svc_log, LOG_SERVICE_ID, LOG_SERVICE_MAJOR, LOG_SERVICE_MINOR);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_FMT)
|
||||
#error "mods/svc/log.hpp requires add_mod(... FEATURES fmt)"
|
||||
#endif
|
||||
|
||||
#include <mods/svc/log.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace mods::log {
|
||||
|
||||
template <typename... Args>
|
||||
void write(LogLevel level, fmt::format_string<Args...> formatString, Args&&... args) {
|
||||
const auto message = fmt::format(formatString, std::forward<Args>(args)...);
|
||||
svc_log->write(mod_ctx, level, message.c_str());
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void trace(fmt::format_string<Args...> formatString, Args&&... args) {
|
||||
write(LOG_LEVEL_TRACE, formatString, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void debug(fmt::format_string<Args...> formatString, Args&&... args) {
|
||||
write(LOG_LEVEL_DEBUG, formatString, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void info(fmt::format_string<Args...> formatString, Args&&... args) {
|
||||
write(LOG_LEVEL_INFO, formatString, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void warn(fmt::format_string<Args...> formatString, Args&&... args) {
|
||||
write(LOG_LEVEL_WARN, formatString, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void error(fmt::format_string<Args...> formatString, Args&&... args) {
|
||||
write(LOG_LEVEL_ERROR, formatString, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
} // namespace mods::log
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay"
|
||||
#define OVERLAY_SERVICE_MAJOR 1u
|
||||
#define OVERLAY_SERVICE_MINOR 0u
|
||||
@@ -46,13 +50,5 @@ typedef struct OverlayService {
|
||||
ModResult (*remove)(ModContext* ctx, OverlayHandle handle);
|
||||
} OverlayService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<OverlayService> {
|
||||
static constexpr const char* id = OVERLAY_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = OVERLAY_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = OVERLAY_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(
|
||||
OverlayService, svc_overlay, OVERLAY_SERVICE_ID, OVERLAY_SERVICE_MAJOR, OVERLAY_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Read-only access to the res/ tree of the calling mod's own bundle. Reload serves the new
|
||||
* bundle's contents. For writable storage, use HostService::mod_dir.
|
||||
@@ -41,13 +45,5 @@ typedef struct ResourceService {
|
||||
void (*free)(ModContext* ctx, ResourceBuffer* buffer);
|
||||
} ResourceService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<ResourceService> {
|
||||
static constexpr const char* id = RESOURCE_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = RESOURCE_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = RESOURCE_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(ResourceService, svc_resource, RESOURCE_SERVICE_ID, RESOURCE_SERVICE_MAJOR,
|
||||
RESOURCE_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture"
|
||||
#define TEXTURE_SERVICE_MAJOR 1u
|
||||
#define TEXTURE_SERVICE_MINOR 0u
|
||||
@@ -70,20 +74,12 @@ typedef struct TextureService {
|
||||
* "tex1_{w}x{h}_{hash}_{fmt}.dds"); "_mipN" sidecars next to it are picked up automatically.
|
||||
* The file is decoded lazily on first use by the renderer.
|
||||
*/
|
||||
ModResult (*register_file)(ModContext* ctx, const char* bundle_path,
|
||||
TextureReplacementHandle* out_handle);
|
||||
ModResult (*register_file)(
|
||||
ModContext* ctx, const char* bundle_path, TextureReplacementHandle* out_handle);
|
||||
|
||||
/* Remove a replacement previously registered by the calling mod. */
|
||||
ModResult (*unregister)(ModContext* ctx, TextureReplacementHandle handle);
|
||||
} TextureService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<TextureService> {
|
||||
static constexpr const char* id = TEXTURE_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = TEXTURE_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = TEXTURE_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(
|
||||
TextureService, svc_texture, TEXTURE_SERVICE_ID, TEXTURE_SERVICE_MAJOR, TEXTURE_SERVICE_MINOR);
|
||||
|
||||
+24
-13
@@ -3,13 +3,17 @@
|
||||
#include <mods/api.h>
|
||||
#include <mods/svc/config.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
|
||||
#define UI_SERVICE_MAJOR 1u
|
||||
#define UI_SERVICE_MINOR 0u
|
||||
#define UI_SERVICE_MINOR 1u
|
||||
|
||||
/*
|
||||
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, scoped
|
||||
* RCSS stylesheets and menu bar tabs.
|
||||
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts,
|
||||
* scoped RCSS stylesheets and menu bar tabs.
|
||||
*
|
||||
* All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI
|
||||
* callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with
|
||||
@@ -208,6 +212,19 @@ typedef struct UiMenuTabDesc {
|
||||
|
||||
#define UI_MENU_TAB_DESC_INIT {sizeof(UiMenuTabDesc), NULL, NULL, NULL}
|
||||
|
||||
typedef struct UiToastDesc {
|
||||
uint32_t struct_size;
|
||||
/* Optional RCSS class, such as "warning" or a custom mod-defined type. */
|
||||
const char* type;
|
||||
/* Optional RML. At least one of title_rml or body_rml must be non-empty. */
|
||||
const char* title_rml;
|
||||
const char* body_rml;
|
||||
/* How long the toast remains open; 0 uses the default of 5000 ms. */
|
||||
uint32_t duration_ms;
|
||||
} UiToastDesc;
|
||||
|
||||
#define UI_TOAST_DESC_INIT {sizeof(UiToastDesc), NULL, NULL, NULL, 0u}
|
||||
|
||||
typedef struct UiService {
|
||||
ServiceHeader header;
|
||||
|
||||
@@ -271,15 +288,9 @@ typedef struct UiService {
|
||||
ModResult (*register_menu_tab)(
|
||||
ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab);
|
||||
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
|
||||
|
||||
/* Enqueue a toast notification. */
|
||||
ModResult (*push_toast)(ModContext* ctx, const UiToastDesc* desc);
|
||||
} UiService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<UiService> {
|
||||
static constexpr const char* id = UI_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = UI_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = UI_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(UiService, svc_ui, UI_SERVICE_ID, UI_SERVICE_MAJOR, UI_SERVICE_MINOR);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
#define WINDOW_SERVICE_ID "dev.twilitrealm.dusklight.window"
|
||||
#define WINDOW_SERVICE_MAJOR 1u
|
||||
#define WINDOW_SERVICE_MINOR 0u
|
||||
|
||||
#define WINDOW_POSITION_UNDEFINED INT32_MIN
|
||||
|
||||
typedef uint64_t WindowHandle;
|
||||
|
||||
typedef enum WindowFlags {
|
||||
WINDOW_FLAG_NONE = 0u,
|
||||
WINDOW_FLAG_RESIZABLE = 1u << 0u,
|
||||
WINDOW_FLAG_HIDDEN = 1u << 1u,
|
||||
WINDOW_FLAG_BORDERLESS = 1u << 2u,
|
||||
WINDOW_FLAG_ALWAYS_ON_TOP = 1u << 3u,
|
||||
WINDOW_FLAG_TRANSPARENT = 1u << 4u,
|
||||
} WindowFlags;
|
||||
|
||||
typedef enum WindowEventType {
|
||||
WINDOW_EVENT_CLOSE_REQUESTED = 0,
|
||||
WINDOW_EVENT_RESIZED = 1,
|
||||
WINDOW_EVENT_MOVED = 2,
|
||||
WINDOW_EVENT_FOCUS_GAINED = 3,
|
||||
WINDOW_EVENT_FOCUS_LOST = 4,
|
||||
WINDOW_EVENT_SHOWN = 5,
|
||||
WINDOW_EVENT_HIDDEN = 6,
|
||||
} WindowEventType;
|
||||
|
||||
typedef struct WindowEvent {
|
||||
uint32_t struct_size;
|
||||
WindowEventType type;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t pixel_width;
|
||||
uint32_t pixel_height;
|
||||
float display_scale;
|
||||
} WindowEvent;
|
||||
|
||||
typedef void (*WindowEventFn)(
|
||||
ModContext* ctx, WindowHandle window, const WindowEvent* event, void* user_data);
|
||||
|
||||
typedef struct WindowDesc {
|
||||
uint32_t struct_size;
|
||||
const char* title;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
uint32_t flags;
|
||||
WindowEventFn on_event;
|
||||
void* user_data;
|
||||
} WindowDesc;
|
||||
|
||||
#define WINDOW_DESC_INIT \
|
||||
{sizeof(WindowDesc), NULL, 640u, 480u, WINDOW_POSITION_UNDEFINED, WINDOW_POSITION_UNDEFINED, \
|
||||
WINDOW_FLAG_RESIZABLE | WINDOW_FLAG_HIDDEN, NULL, NULL}
|
||||
|
||||
typedef struct WindowInfo {
|
||||
uint32_t struct_size;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t pixel_width;
|
||||
uint32_t pixel_height;
|
||||
float display_scale;
|
||||
bool visible;
|
||||
bool focused;
|
||||
} WindowInfo;
|
||||
|
||||
#define WINDOW_INFO_INIT {sizeof(WindowInfo), 0, 0, 0u, 0u, 0u, 0u, 1.0f, false, false}
|
||||
|
||||
/*
|
||||
* Auxiliary native windows. All functions and callbacks run on the game thread. Window close
|
||||
* events are requests; the window remains alive until destroy_window is called. Any graphics
|
||||
* present target attached to a window must be unregistered before the window can be destroyed;
|
||||
* at most one present target may be attached to a window at a time.
|
||||
*/
|
||||
typedef struct WindowService {
|
||||
ServiceHeader header;
|
||||
|
||||
ModResult (*create_window)(ModContext* ctx, const WindowDesc* desc, WindowHandle* out_window);
|
||||
ModResult (*destroy_window)(ModContext* ctx, WindowHandle window);
|
||||
ModResult (*show_window)(ModContext* ctx, WindowHandle window);
|
||||
ModResult (*hide_window)(ModContext* ctx, WindowHandle window);
|
||||
ModResult (*set_title)(ModContext* ctx, WindowHandle window, const char* title);
|
||||
ModResult (*set_size)(ModContext* ctx, WindowHandle window, uint32_t width, uint32_t height);
|
||||
ModResult (*get_info)(ModContext* ctx, WindowHandle window, WindowInfo* out_info);
|
||||
} WindowService;
|
||||
|
||||
MOD_DECLARE_SERVICE(
|
||||
WindowService, svc_window, WINDOW_SERVICE_ID, WINDOW_SERVICE_MAJOR, WINDOW_SERVICE_MINOR);
|
||||
+584
-39
@@ -1,5 +1,6 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/gfx.hpp"
|
||||
@@ -7,11 +8,16 @@
|
||||
#include "mods/svc/gfx.h"
|
||||
|
||||
#include <aurora/gfx.hpp>
|
||||
#include <aurora/webgpu.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -25,6 +31,7 @@ enum class GfxSlotKind : uint8_t {
|
||||
DrawType,
|
||||
StageHook,
|
||||
ComputeType,
|
||||
PresentTarget,
|
||||
};
|
||||
|
||||
enum class GfxStreamBuffer : uint8_t {
|
||||
@@ -34,6 +41,33 @@ enum class GfxStreamBuffer : uint8_t {
|
||||
Storage,
|
||||
};
|
||||
|
||||
enum class PresentTargetStatus : uint8_t {
|
||||
Pending,
|
||||
Ready,
|
||||
TransientUnavailable,
|
||||
Lost,
|
||||
Error,
|
||||
};
|
||||
|
||||
struct PresentTargetState {
|
||||
wgpu::Surface surface;
|
||||
wgpu::SurfaceConfiguration configuration;
|
||||
wgpu::Texture currentTexture;
|
||||
wgpu::TextureView currentView;
|
||||
std::atomic<PresentTargetStatus> status = PresentTargetStatus::Pending;
|
||||
bool configured = false;
|
||||
bool presentPending = false;
|
||||
bool vsync = true;
|
||||
};
|
||||
|
||||
bool present_target_failed(const std::shared_ptr<PresentTargetState>& state) {
|
||||
if (state == nullptr) {
|
||||
return true;
|
||||
}
|
||||
const auto status = state->status.load(std::memory_order_acquire);
|
||||
return status == PresentTargetStatus::Lost || status == PresentTargetStatus::Error;
|
||||
}
|
||||
|
||||
struct GfxSlot {
|
||||
GfxSlotKind kind = GfxSlotKind::DrawType;
|
||||
ModContext* ownerContext = nullptr;
|
||||
@@ -48,13 +82,21 @@ struct GfxSlot {
|
||||
|
||||
GfxComputeFn computeFn = nullptr;
|
||||
aurora::gfx::EncoderTaskId auroraTaskId = aurora::gfx::InvalidEncoderTask;
|
||||
|
||||
GfxPresentFn presentFn = nullptr;
|
||||
std::shared_ptr<PresentTargetState> presentState;
|
||||
WindowHandle window = 0;
|
||||
uint32_t targetWidth = 0;
|
||||
uint32_t targetHeight = 0;
|
||||
WGPUTextureUsage targetUsage = WGPUTextureUsage_RenderAttachment;
|
||||
WGPUTextureFormat preferredFormat = WGPUTextureFormat_Undefined;
|
||||
WGPUCompositeAlphaMode preferredAlphaMode = WGPUCompositeAlphaMode_Auto;
|
||||
uint32_t lastPresentFrame = UINT32_MAX;
|
||||
};
|
||||
|
||||
struct WorkerFailure {
|
||||
std::string modId;
|
||||
std::string message;
|
||||
std::vector<aurora::gfx::DrawTypeId> drawIds;
|
||||
std::vector<aurora::gfx::EncoderTaskId> taskIds;
|
||||
};
|
||||
|
||||
std::mutex s_mutex;
|
||||
@@ -84,20 +126,23 @@ GfxSlot* resolve_owned_slot_locked(LoadedMod& mod, uint64_t handle, GfxSlotKind
|
||||
return &entry->value;
|
||||
}
|
||||
|
||||
void collect_mod_slots_locked(LoadedMod& owner, std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
||||
void collect_mod_types_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) {
|
||||
s_slots.for_each([&](uint64_t, const auto& entry) {
|
||||
if (entry.owner != &owner) {
|
||||
return;
|
||||
}
|
||||
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 &&
|
||||
} else if ((slot.kind == GfxSlotKind::ComputeType ||
|
||||
slot.kind == GfxSlotKind::PresentTarget) &&
|
||||
slot.auroraTaskId != aurora::gfx::InvalidEncoderTask)
|
||||
{
|
||||
taskIds.push_back(slot.auroraTaskId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void unregister_aurora_types(const std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
||||
@@ -116,7 +161,6 @@ void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPass
|
||||
GfxDrawFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
ModContext* modContext = nullptr;
|
||||
LoadedMod* owner = nullptr;
|
||||
std::string ownerId;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
@@ -128,7 +172,6 @@ void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPass
|
||||
fn = slot.drawFn;
|
||||
userData = slot.userData;
|
||||
modContext = slot.ownerContext;
|
||||
owner = entry->owner;
|
||||
ownerId = slot.ownerId;
|
||||
}
|
||||
|
||||
@@ -164,7 +207,6 @@ void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPass
|
||||
.modId = std::move(ownerId),
|
||||
.message = std::move(failure),
|
||||
};
|
||||
collect_mod_slots_locked(*owner, record.drawIds, record.taskIds);
|
||||
s_workerFailures.push_back(std::move(record));
|
||||
}
|
||||
|
||||
@@ -174,7 +216,6 @@ void compute_trampoline(const aurora::gfx::EncoderTaskContext& ctx, const wgpu::
|
||||
GfxComputeFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
ModContext* modContext = nullptr;
|
||||
LoadedMod* owner = nullptr;
|
||||
std::string ownerId;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
@@ -186,7 +227,6 @@ void compute_trampoline(const aurora::gfx::EncoderTaskContext& ctx, const wgpu::
|
||||
fn = slot.computeFn;
|
||||
userData = slot.userData;
|
||||
modContext = slot.ownerContext;
|
||||
owner = entry->owner;
|
||||
ownerId = slot.ownerId;
|
||||
}
|
||||
|
||||
@@ -216,10 +256,235 @@ void compute_trampoline(const aurora::gfx::EncoderTaskContext& ctx, const wgpu::
|
||||
.modId = std::move(ownerId),
|
||||
.message = std::move(failure),
|
||||
};
|
||||
collect_mod_slots_locked(*owner, record.drawIds, record.taskIds);
|
||||
s_workerFailures.push_back(std::move(record));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool contains(const T* values, size_t count, T value) {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (values[i] == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool configure_present_target(const std::shared_ptr<PresentTargetState>& state,
|
||||
const aurora::gfx::EncoderTaskContext& ctx, uint32_t width, uint32_t height,
|
||||
WGPUTextureUsage requestedUsage, WGPUTextureFormat preferredFormat,
|
||||
WGPUCompositeAlphaMode preferredAlphaMode, bool vsync) {
|
||||
if (width == 0 || height == 0) {
|
||||
state->status.store(PresentTargetStatus::TransientUnavailable, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
if (!state->surface) {
|
||||
state->status.store(PresentTargetStatus::Error, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto adapter = ctx.device.GetAdapter();
|
||||
wgpu::SurfaceCapabilities capabilities;
|
||||
if (!adapter ||
|
||||
state->surface.GetCapabilities(adapter, &capabilities) != wgpu::Status::Success ||
|
||||
capabilities.formatCount == 0 || capabilities.presentModeCount == 0 ||
|
||||
capabilities.alphaModeCount == 0)
|
||||
{
|
||||
state->status.store(PresentTargetStatus::Error, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto format = static_cast<wgpu::TextureFormat>(preferredFormat);
|
||||
if (format == wgpu::TextureFormat::Undefined ||
|
||||
!contains(capabilities.formats, capabilities.formatCount, format))
|
||||
{
|
||||
format = aurora::gfx::color_format();
|
||||
}
|
||||
if (format == wgpu::TextureFormat::Undefined ||
|
||||
!contains(capabilities.formats, capabilities.formatCount, format))
|
||||
{
|
||||
format = capabilities.formats[0];
|
||||
}
|
||||
|
||||
const auto presentMode = aurora::webgpu::select_present_mode(capabilities);
|
||||
|
||||
auto alphaMode = static_cast<wgpu::CompositeAlphaMode>(preferredAlphaMode);
|
||||
if (alphaMode != wgpu::CompositeAlphaMode::Auto &&
|
||||
!contains(capabilities.alphaModes, capabilities.alphaModeCount, alphaMode))
|
||||
{
|
||||
alphaMode = capabilities.alphaModes[0];
|
||||
}
|
||||
|
||||
auto usage = static_cast<wgpu::TextureUsage>(requestedUsage);
|
||||
if (usage == wgpu::TextureUsage::None) {
|
||||
usage = wgpu::TextureUsage::RenderAttachment;
|
||||
}
|
||||
if ((usage & wgpu::TextureUsage::RenderAttachment) == wgpu::TextureUsage::None ||
|
||||
(usage & ~capabilities.usages) != wgpu::TextureUsage::None)
|
||||
{
|
||||
state->status.store(PresentTargetStatus::Error, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
|
||||
state->configuration = wgpu::SurfaceConfiguration{
|
||||
.device = ctx.device,
|
||||
.format = format,
|
||||
.usage = usage,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.alphaMode = alphaMode,
|
||||
.presentMode = presentMode,
|
||||
};
|
||||
state->surface.Configure(&state->configuration);
|
||||
state->configured = true;
|
||||
state->vsync = vsync;
|
||||
state->status.store(PresentTargetStatus::Pending, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
void present_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));
|
||||
GfxPresentFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
ModContext* modContext = nullptr;
|
||||
std::string ownerId;
|
||||
std::shared_ptr<PresentTargetState> state;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
WGPUTextureUsage usage = WGPUTextureUsage_RenderAttachment;
|
||||
WGPUTextureFormat preferredFormat = WGPUTextureFormat_Undefined;
|
||||
WGPUCompositeAlphaMode preferredAlphaMode = WGPUCompositeAlphaMode_Auto;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* entry = resolve_entry_locked(handle, GfxSlotKind::PresentTarget);
|
||||
if (entry == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto& slot = entry->value;
|
||||
fn = slot.presentFn;
|
||||
userData = slot.userData;
|
||||
modContext = slot.ownerContext;
|
||||
ownerId = slot.ownerId;
|
||||
state = slot.presentState;
|
||||
width = slot.targetWidth;
|
||||
height = slot.targetHeight;
|
||||
usage = slot.targetUsage;
|
||||
preferredFormat = slot.preferredFormat;
|
||||
preferredAlphaMode = slot.preferredAlphaMode;
|
||||
}
|
||||
if (fn == nullptr || state == nullptr || present_target_failed(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->presentPending = false;
|
||||
state->currentView = {};
|
||||
state->currentTexture = {};
|
||||
const bool vsync = aurora::webgpu::vsync_enabled();
|
||||
if (!state->configured || state->configuration.width != width ||
|
||||
state->configuration.height != height || state->vsync != vsync)
|
||||
{
|
||||
if (!configure_present_target(
|
||||
state, ctx, width, height, usage, preferredFormat, preferredAlphaMode, vsync))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
wgpu::SurfaceTexture surfaceTexture;
|
||||
state->surface.GetCurrentTexture(&surfaceTexture);
|
||||
if (surfaceTexture.status != wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal &&
|
||||
surfaceTexture.status != wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal)
|
||||
{
|
||||
if (surfaceTexture.status == wgpu::SurfaceGetCurrentTextureStatus::Lost) {
|
||||
state->status.store(PresentTargetStatus::Lost, std::memory_order_release);
|
||||
state->configured = false;
|
||||
} else if (surfaceTexture.status == wgpu::SurfaceGetCurrentTextureStatus::Outdated) {
|
||||
state->status.store(
|
||||
PresentTargetStatus::TransientUnavailable, std::memory_order_release);
|
||||
state->configured = false;
|
||||
} else if (surfaceTexture.status == wgpu::SurfaceGetCurrentTextureStatus::Error) {
|
||||
state->status.store(PresentTargetStatus::Error, std::memory_order_release);
|
||||
state->configured = false;
|
||||
} else {
|
||||
state->status.store(
|
||||
PresentTargetStatus::TransientUnavailable, std::memory_order_release);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state->currentTexture = std::move(surfaceTexture.texture);
|
||||
state->currentView = state->currentTexture.CreateView();
|
||||
if (!state->currentTexture || !state->currentView) {
|
||||
state->status.store(PresentTargetStatus::Error, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
const GfxPresentContext presentContext{
|
||||
.struct_size = sizeof(GfxPresentContext),
|
||||
.device = ctx.device.Get(),
|
||||
.queue = ctx.queue.Get(),
|
||||
.encoder = cmd.Get(),
|
||||
.target_texture = state->currentTexture.Get(),
|
||||
.target_view = state->currentView.Get(),
|
||||
.target_format = static_cast<WGPUTextureFormat>(state->configuration.format),
|
||||
.target_width = width,
|
||||
.target_height = height,
|
||||
.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, &presentContext, payload, payloadSize, userData);
|
||||
state->presentPending = true;
|
||||
state->status.store(PresentTargetStatus::Ready, std::memory_order_release);
|
||||
if (surfaceTexture.status == wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal) {
|
||||
state->configured = false;
|
||||
}
|
||||
return;
|
||||
} catch (const std::exception& e) {
|
||||
failure = fmt::format("exception in gfx present callback: {}", e.what());
|
||||
} catch (...) {
|
||||
failure = "unknown exception in gfx present callback";
|
||||
}
|
||||
|
||||
state->presentPending = false;
|
||||
state->status.store(PresentTargetStatus::Error, std::memory_order_release);
|
||||
std::lock_guard lock{s_mutex};
|
||||
s_workerFailures.push_back(WorkerFailure{
|
||||
.modId = std::move(ownerId),
|
||||
.message = std::move(failure),
|
||||
});
|
||||
}
|
||||
|
||||
void present_after_submit_trampoline(
|
||||
const aurora::gfx::EncoderTaskCompletionContext&, const void*, size_t, void* userdata) {
|
||||
const auto handle = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(userdata));
|
||||
std::shared_ptr<PresentTargetState> state;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
const auto* entry = resolve_entry_locked(handle, GfxSlotKind::PresentTarget);
|
||||
if (entry == nullptr) {
|
||||
return;
|
||||
}
|
||||
state = entry->value.presentState;
|
||||
}
|
||||
if (state == nullptr || !state->presentPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool presented = state->surface.Present();
|
||||
state->presentPending = false;
|
||||
state->currentView = {};
|
||||
state->currentTexture = {};
|
||||
if (!presented) {
|
||||
state->configured = false;
|
||||
state->status.store(PresentTargetStatus::Error, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModResult gfx_register_draw_type(
|
||||
@@ -449,6 +714,158 @@ ModResult gfx_push_compute(
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_register_present_target(LoadedMod& mod, wgpu::Surface surface, WindowHandle window,
|
||||
const GfxPresentTargetDesc& desc, uint64_t& outHandle) {
|
||||
outHandle = 0;
|
||||
auto state = std::make_shared<PresentTargetState>();
|
||||
state->surface = std::move(surface);
|
||||
|
||||
uint64_t handle = 0;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
handle = s_slots.emplace(mod, GfxSlot{
|
||||
.kind = GfxSlotKind::PresentTarget,
|
||||
.ownerContext = mod.context.get(),
|
||||
.ownerId = mod.metadata.id,
|
||||
.userData = desc.user_data,
|
||||
.presentFn = desc.render,
|
||||
.presentState = state,
|
||||
.window = window,
|
||||
.targetWidth = desc.width,
|
||||
.targetHeight = desc.height,
|
||||
.targetUsage = desc.usage == WGPUTextureUsage_None ?
|
||||
WGPUTextureUsage_RenderAttachment :
|
||||
desc.usage,
|
||||
.preferredFormat = desc.preferred_format,
|
||||
.preferredAlphaMode = desc.preferred_alpha_mode,
|
||||
});
|
||||
}
|
||||
|
||||
const auto auroraId =
|
||||
aurora::gfx::register_encoder_task_type(aurora::gfx::EncoderTaskDescriptor{
|
||||
.label = desc.label,
|
||||
.callback = present_trampoline,
|
||||
.userdata = reinterpret_cast<void*>(static_cast<uintptr_t>(handle)),
|
||||
.afterSubmit = present_after_submit_trampoline,
|
||||
});
|
||||
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};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::PresentTarget);
|
||||
if (slot == nullptr) {
|
||||
aurora::gfx::unregister_encoder_task_type(auroraId);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
slot->auroraTaskId = auroraId;
|
||||
}
|
||||
outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_unregister_present_target(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::PresentTarget);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auroraId = slot->auroraTaskId;
|
||||
}
|
||||
|
||||
aurora::gfx::unregister_encoder_task_type(auroraId);
|
||||
aurora::gfx::synchronize();
|
||||
|
||||
std::optional<GfxSlotMap::Entry> removed;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
removed = s_slots.take_owned(handle, mod);
|
||||
}
|
||||
if (!removed.has_value()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (removed->value.presentState != nullptr && removed->value.presentState->configured) {
|
||||
removed->value.presentState->surface.Unconfigure();
|
||||
}
|
||||
if (removed->value.window != 0) {
|
||||
svc::window_release_for_graphics(mod, removed->value.window);
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_resize_present_target(
|
||||
LoadedMod& mod, uint64_t handle, uint32_t width, uint32_t height) {
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::PresentTarget);
|
||||
if (slot == nullptr || width == 0 || height == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (slot->window != 0) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
slot->targetWidth = width;
|
||||
slot->targetHeight = height;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_push_present(
|
||||
LoadedMod& mod, uint64_t handle, const void* payload, size_t payloadSize) {
|
||||
WindowHandle window = 0;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::PresentTarget);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (present_target_failed(slot->presentState)) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
window = slot->window;
|
||||
}
|
||||
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
if (window != 0 && !svc::window_get_pixel_size(mod, window, width, height)) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
aurora::gfx::EncoderTaskId auroraId = aurora::gfx::InvalidEncoderTask;
|
||||
const uint32_t frame = aurora::gfx::current_frame();
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::PresentTarget);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (present_target_failed(slot->presentState)) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
if (slot->lastPresentFrame == frame) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
if (window != 0) {
|
||||
slot->targetWidth = width;
|
||||
slot->targetHeight = height;
|
||||
}
|
||||
auroraId = slot->auroraTaskId;
|
||||
}
|
||||
if (!aurora::gfx::push_encoder_task(auroraId, payload, payloadSize)) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
if (auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::PresentTarget)) {
|
||||
slot->lastPresentFrame = frame;
|
||||
}
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void gfx_run_stage(
|
||||
GfxStage stage, const view_class* gameView, const view_port_class* gameViewport) {
|
||||
struct StageEntry {
|
||||
@@ -518,6 +935,37 @@ void gfx_run_stage(
|
||||
}
|
||||
}
|
||||
|
||||
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_types_locked(mod, drawIds, taskIds);
|
||||
}
|
||||
unregister_aurora_types(drawIds, taskIds);
|
||||
if (!drawIds.empty() || !taskIds.empty()) {
|
||||
aurora::gfx::synchronize();
|
||||
}
|
||||
|
||||
std::vector<GfxSlotMap::Entry> entries;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
entries = s_slots.take_all(mod);
|
||||
}
|
||||
for (auto& entry : entries) {
|
||||
auto& slot = entry.value;
|
||||
if (slot.kind != GfxSlotKind::PresentTarget) {
|
||||
continue;
|
||||
}
|
||||
if (slot.presentState != nullptr && slot.presentState->configured) {
|
||||
slot.presentState->surface.Unconfigure();
|
||||
}
|
||||
if (slot.window != 0) {
|
||||
svc::window_release_for_graphics(mod, slot.window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_drain_worker_failures() {
|
||||
std::vector<WorkerFailure> failures;
|
||||
{
|
||||
@@ -528,18 +976,10 @@ void gfx_drain_worker_failures() {
|
||||
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) {
|
||||
gfx_remove_mod(mod);
|
||||
fail_mod(mod, MOD_ERROR, failure.message);
|
||||
break;
|
||||
}
|
||||
@@ -547,43 +987,46 @@ void gfx_drain_worker_failures() {
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
constexpr uint32_t v0Size = offsetof(GfxDeviceInfo, instance);
|
||||
if (outInfo == nullptr || outInfo->struct_size < v0Size) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const uint32_t structSize = outInfo->struct_size;
|
||||
*outInfo = GfxDeviceInfo{.struct_size = structSize};
|
||||
outInfo->device = nullptr;
|
||||
outInfo->queue = nullptr;
|
||||
outInfo->color_format = WGPUTextureFormat_Undefined;
|
||||
outInfo->depth_format = WGPUTextureFormat_Undefined;
|
||||
outInfo->sample_count = 1;
|
||||
outInfo->uses_reversed_z = false;
|
||||
if (structSize >= sizeof(GfxDeviceInfo)) {
|
||||
outInfo->instance = nullptr;
|
||||
outInfo->adapter = nullptr;
|
||||
}
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
outInfo->device = aurora::gfx::device().Get();
|
||||
const auto device = aurora::gfx::device();
|
||||
outInfo->device = 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();
|
||||
if (structSize >= sizeof(GfxDeviceInfo)) {
|
||||
const auto adapter = device.GetAdapter();
|
||||
const auto instance = adapter ? adapter.GetInstance() : wgpu::Instance{};
|
||||
outInfo->instance = instance.Get();
|
||||
outInfo->adapter = adapter.Get();
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -594,6 +1037,103 @@ void* gfx_get_proc_address(ModContext* context, const char* name) {
|
||||
return reinterpret_cast<void*>(wgpuGetProcAddress(WGPUStringView{name, WGPU_STRLEN}));
|
||||
}
|
||||
|
||||
bool valid_present_desc(const GfxPresentTargetDesc* desc) {
|
||||
return desc != nullptr && desc->struct_size >= sizeof(GfxPresentTargetDesc) &&
|
||||
desc->render != nullptr;
|
||||
}
|
||||
|
||||
ModResult gfx_register_present_target_impl(ModContext* context, WGPUSurface surface,
|
||||
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || surface == nullptr || !valid_present_desc(desc) || outHandle == nullptr ||
|
||||
desc->width == 0 || desc->height == 0)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
uint64_t handle = 0;
|
||||
const auto result = gfx_register_present_target(*mod, wgpu::Surface{surface}, 0, *desc, handle);
|
||||
if (result == MOD_OK) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult gfx_register_window_present_target_impl(ModContext* context, WindowHandle window,
|
||||
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || window == 0 || !valid_present_desc(desc) || outHandle == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SDL_Window* sdlWindow = nullptr;
|
||||
const auto acquireResult = window_acquire_for_graphics(*mod, window, sdlWindow);
|
||||
if (acquireResult != MOD_OK) {
|
||||
return acquireResult;
|
||||
}
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
if (!window_get_pixel_size(*mod, window, width, height)) {
|
||||
window_release_for_graphics(*mod, window);
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
const auto device = aurora::gfx::device();
|
||||
const auto adapter = device.GetAdapter();
|
||||
const auto instance = adapter ? adapter.GetInstance() : wgpu::Instance{};
|
||||
auto surface = aurora::webgpu::create_window_surface(instance, sdlWindow, desc->label);
|
||||
if (!surface) {
|
||||
window_release_for_graphics(*mod, window);
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
auto windowDesc = *desc;
|
||||
windowDesc.width = width;
|
||||
windowDesc.height = height;
|
||||
uint64_t handle = 0;
|
||||
const auto result =
|
||||
gfx_register_present_target(*mod, std::move(surface), window, windowDesc, handle);
|
||||
if (result != MOD_OK) {
|
||||
window_release_for_graphics(*mod, window);
|
||||
return result;
|
||||
}
|
||||
*outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_resize_present_target_impl(
|
||||
ModContext* context, GfxPresentTargetHandle handle, uint32_t width, uint32_t height) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0 || width == 0 || height == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_resize_present_target(*mod, handle, width, height);
|
||||
}
|
||||
|
||||
ModResult gfx_unregister_present_target_impl(ModContext* context, GfxPresentTargetHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_unregister_present_target(*mod, handle);
|
||||
}
|
||||
|
||||
ModResult gfx_push_present_impl(
|
||||
ModContext* context, GfxPresentTargetHandle 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_present(*mod, handle, payload, payloadSize);
|
||||
}
|
||||
|
||||
ModResult gfx_register_draw_type_impl(
|
||||
ModContext* context, const GfxDrawTypeDesc* desc, GfxDrawTypeHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
@@ -782,6 +1322,11 @@ constexpr GfxService s_gfxService{
|
||||
.unregister_stage_hook = gfx_unregister_stage_hook_impl,
|
||||
.resolve_pass = gfx_resolve_pass_impl,
|
||||
.create_pass = gfx_create_pass_impl,
|
||||
.register_present_target = gfx_register_present_target_impl,
|
||||
.register_window_present_target = gfx_register_window_present_target_impl,
|
||||
.resize_present_target = gfx_resize_present_target_impl,
|
||||
.unregister_present_target = gfx_unregister_present_target_impl,
|
||||
.push_present = gfx_push_present_impl,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -209,6 +209,7 @@ void ModLoader::init_services() {
|
||||
&svc::g_uiModule,
|
||||
&svc::g_gameModule,
|
||||
&svc::g_cameraModule,
|
||||
&svc::g_windowModule,
|
||||
&svc::g_gfxModule,
|
||||
})
|
||||
{
|
||||
|
||||
@@ -71,6 +71,7 @@ extern const ServiceModule g_configModule;
|
||||
extern const ServiceModule g_uiModule;
|
||||
extern const ServiceModule g_gameModule;
|
||||
extern const ServiceModule g_cameraModule;
|
||||
extern const ServiceModule g_windowModule;
|
||||
extern const ServiceModule g_gfxModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -1321,6 +1322,27 @@ ModResult ui_unregister_menu_tab(ModContext* context, UiMenuTabHandle tab) {
|
||||
return ui_impl::ui_unregister_menu_tab(*mod, tab);
|
||||
}
|
||||
|
||||
ModResult ui_push_toast(ModContext* context, const UiToastDesc* desc) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiToastDesc) ||
|
||||
((desc->title_rml == nullptr || desc->title_rml[0] == '\0') &&
|
||||
(desc->body_rml == nullptr || desc->body_rml[0] == '\0')))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
constexpr uint32_t kDefaultDurationMs = 5000;
|
||||
const uint32_t durationMs = desc->duration_ms == 0 ? kDefaultDurationMs : desc->duration_ms;
|
||||
ui::push_toast({
|
||||
.type = desc->type != nullptr ? desc->type : "",
|
||||
.title = desc->title_rml != nullptr ? desc->title_rml : "",
|
||||
.content = desc->body_rml != nullptr ? desc->body_rml : "",
|
||||
.duration = std::chrono::milliseconds{durationMs},
|
||||
.modId = mod->metadata.id,
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult ui_dialog_set_body(ModContext* context, UiDialogHandle dialog, const char* bodyRml) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || dialog == 0 || bodyRml == nullptr) {
|
||||
@@ -1371,6 +1393,7 @@ constexpr UiService s_uiService{
|
||||
.unregister_styles = ui_unregister_styles,
|
||||
.register_menu_tab = ui_register_menu_tab,
|
||||
.unregister_menu_tab = ui_unregister_menu_tab,
|
||||
.push_toast = ui_push_toast,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
#include "window.hpp"
|
||||
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::window");
|
||||
|
||||
struct WindowSlot {
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_WindowID windowId = 0;
|
||||
WindowEventFn onEvent = nullptr;
|
||||
void* userData = nullptr;
|
||||
uint32_t graphicsRefs = 0;
|
||||
};
|
||||
|
||||
SlotMap<WindowSlot> s_windows;
|
||||
std::unordered_map<SDL_WindowID, WindowHandle> s_windowsById;
|
||||
|
||||
WindowSlot* resolve_window(LoadedMod& mod, WindowHandle handle) {
|
||||
auto* entry = s_windows.find_owned(handle, mod);
|
||||
return entry != nullptr ? &entry->value : nullptr;
|
||||
}
|
||||
|
||||
bool populate_info(SDL_Window* window, WindowInfo& info) {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int pixelWidth = 0;
|
||||
int pixelHeight = 0;
|
||||
if (!SDL_GetWindowPosition(window, &x, &y) || !SDL_GetWindowSize(window, &width, &height) ||
|
||||
!SDL_GetWindowSizeInPixels(window, &pixelWidth, &pixelHeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const auto flags = SDL_GetWindowFlags(window);
|
||||
info.x = x;
|
||||
info.y = y;
|
||||
info.width = static_cast<uint32_t>(width);
|
||||
info.height = static_cast<uint32_t>(height);
|
||||
info.pixel_width = static_cast<uint32_t>(pixelWidth);
|
||||
info.pixel_height = static_cast<uint32_t>(pixelHeight);
|
||||
info.display_scale = SDL_GetWindowDisplayScale(window);
|
||||
info.visible = (flags & SDL_WINDOW_HIDDEN) == 0u;
|
||||
info.focused = (flags & SDL_WINDOW_INPUT_FOCUS) != 0u;
|
||||
return true;
|
||||
}
|
||||
|
||||
ModResult create_window_impl(ModContext* context, const WindowDesc* desc, WindowHandle* outWindow) {
|
||||
if (outWindow != nullptr) {
|
||||
*outWindow = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(WindowDesc) ||
|
||||
outWindow == nullptr || desc->width == 0 || desc->height == 0)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SDL_WindowFlags flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
|
||||
if ((desc->flags & WINDOW_FLAG_RESIZABLE) != 0u) {
|
||||
flags |= SDL_WINDOW_RESIZABLE;
|
||||
}
|
||||
if ((desc->flags & WINDOW_FLAG_HIDDEN) != 0u) {
|
||||
flags |= SDL_WINDOW_HIDDEN;
|
||||
}
|
||||
if ((desc->flags & WINDOW_FLAG_BORDERLESS) != 0u) {
|
||||
flags |= SDL_WINDOW_BORDERLESS;
|
||||
}
|
||||
if ((desc->flags & WINDOW_FLAG_ALWAYS_ON_TOP) != 0u) {
|
||||
flags |= SDL_WINDOW_ALWAYS_ON_TOP;
|
||||
}
|
||||
if ((desc->flags & WINDOW_FLAG_TRANSPARENT) != 0u) {
|
||||
flags |= SDL_WINDOW_TRANSPARENT;
|
||||
}
|
||||
|
||||
const auto properties = SDL_CreateProperties();
|
||||
if (properties == 0) {
|
||||
Log.error("[{}] create_window: {}", mod->metadata.id, SDL_GetError());
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
const char* title = desc->title != nullptr ? desc->title : mod->metadata.name.c_str();
|
||||
const auto x = desc->x == WINDOW_POSITION_UNDEFINED ? SDL_WINDOWPOS_UNDEFINED : desc->x;
|
||||
const auto y = desc->y == WINDOW_POSITION_UNDEFINED ? SDL_WINDOWPOS_UNDEFINED : desc->y;
|
||||
const bool propertiesSet =
|
||||
SDL_SetStringProperty(properties, SDL_PROP_WINDOW_CREATE_TITLE_STRING, title) &&
|
||||
SDL_SetNumberProperty(properties, SDL_PROP_WINDOW_CREATE_X_NUMBER, x) &&
|
||||
SDL_SetNumberProperty(properties, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y) &&
|
||||
SDL_SetNumberProperty(properties, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, desc->width) &&
|
||||
SDL_SetNumberProperty(properties, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, desc->height) &&
|
||||
SDL_SetNumberProperty(properties, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER, flags) &&
|
||||
SDL_SetBooleanProperty(
|
||||
properties, SDL_PROP_WINDOW_CREATE_EXTERNAL_GRAPHICS_CONTEXT_BOOLEAN, true);
|
||||
if (!propertiesSet) {
|
||||
Log.error("[{}] create_window: {}", mod->metadata.id, SDL_GetError());
|
||||
SDL_DestroyProperties(properties);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
SDL_Window* window = SDL_CreateWindowWithProperties(properties);
|
||||
SDL_DestroyProperties(properties);
|
||||
if (window == nullptr) {
|
||||
Log.error("[{}] create_window: {}", mod->metadata.id, SDL_GetError());
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
const auto windowId = SDL_GetWindowID(window);
|
||||
const auto handle = s_windows.emplace(*mod, WindowSlot{
|
||||
.window = window,
|
||||
.windowId = windowId,
|
||||
.onEvent = desc->on_event,
|
||||
.userData = desc->user_data,
|
||||
});
|
||||
s_windowsById.emplace(windowId, handle);
|
||||
*outWindow = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult destroy_window_impl(ModContext* context, WindowHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* slot = resolve_window(*mod, handle);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (slot->graphicsRefs != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
const auto windowId = slot->windowId;
|
||||
SDL_Window* window = slot->window;
|
||||
s_windowsById.erase(windowId);
|
||||
s_windows.erase_owned(handle, *mod);
|
||||
SDL_DestroyWindow(window);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult show_window_impl(ModContext* context, WindowHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
auto* slot = mod != nullptr ? resolve_window(*mod, handle) : nullptr;
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return SDL_ShowWindow(slot->window) ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult hide_window_impl(ModContext* context, WindowHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
auto* slot = mod != nullptr ? resolve_window(*mod, handle) : nullptr;
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return SDL_HideWindow(slot->window) ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult set_title_impl(ModContext* context, WindowHandle handle, const char* title) {
|
||||
auto* mod = mod_from_context(context);
|
||||
auto* slot = mod != nullptr ? resolve_window(*mod, handle) : nullptr;
|
||||
if (slot == nullptr || title == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return SDL_SetWindowTitle(slot->window, title) ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult set_size_impl(ModContext* context, WindowHandle handle, uint32_t width, uint32_t height) {
|
||||
auto* mod = mod_from_context(context);
|
||||
auto* slot = mod != nullptr ? resolve_window(*mod, handle) : nullptr;
|
||||
if (slot == nullptr || width == 0 || height == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return SDL_SetWindowSize(slot->window, static_cast<int>(width), static_cast<int>(height)) ?
|
||||
MOD_OK :
|
||||
MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult get_info_impl(ModContext* context, WindowHandle handle, WindowInfo* outInfo) {
|
||||
if (outInfo == nullptr || outInfo->struct_size < sizeof(WindowInfo)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const uint32_t structSize = outInfo->struct_size;
|
||||
*outInfo = WindowInfo{.struct_size = structSize};
|
||||
auto* mod = mod_from_context(context);
|
||||
auto* slot = mod != nullptr ? resolve_window(*mod, handle) : nullptr;
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return populate_info(slot->window, *outInfo) ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
void remove_mod_windows(LoadedMod& mod) {
|
||||
auto entries = s_windows.take_all(mod);
|
||||
for (auto& entry : entries) {
|
||||
s_windowsById.erase(entry.value.windowId);
|
||||
SDL_DestroyWindow(entry.value.window);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr WindowService s_windowService{
|
||||
.header = SERVICE_HEADER(WindowService, WINDOW_SERVICE_MAJOR, WINDOW_SERVICE_MINOR),
|
||||
.create_window = create_window_impl,
|
||||
.destroy_window = destroy_window_impl,
|
||||
.show_window = show_window_impl,
|
||||
.hide_window = hide_window_impl,
|
||||
.set_title = set_title_impl,
|
||||
.set_size = set_size_impl,
|
||||
.get_info = get_info_impl,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool window_dispatch_event(const SDL_Event& event) {
|
||||
SDL_Window* eventWindow = SDL_GetWindowFromEvent(&event);
|
||||
if (eventWindow == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto it = s_windowsById.find(SDL_GetWindowID(eventWindow));
|
||||
if (it == s_windowsById.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto handle = it->second;
|
||||
const auto* entry = s_windows.find(handle);
|
||||
if (entry == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
WindowEventType type;
|
||||
bool exposed = true;
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
type = WINDOW_EVENT_CLOSE_REQUESTED;
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_RESIZED:
|
||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
|
||||
type = WINDOW_EVENT_RESIZED;
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_MOVED:
|
||||
type = WINDOW_EVENT_MOVED;
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
type = WINDOW_EVENT_FOCUS_GAINED;
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
type = WINDOW_EVENT_FOCUS_LOST;
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_SHOWN:
|
||||
type = WINDOW_EVENT_SHOWN;
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_HIDDEN:
|
||||
type = WINDOW_EVENT_HIDDEN;
|
||||
break;
|
||||
default:
|
||||
exposed = false;
|
||||
break;
|
||||
}
|
||||
if (!exposed || entry->value.onEvent == nullptr || !entry->owner->active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
WindowInfo info = WINDOW_INFO_INIT;
|
||||
populate_info(entry->value.window, info);
|
||||
const WindowEvent windowEvent{
|
||||
.struct_size = sizeof(WindowEvent),
|
||||
.type = type,
|
||||
.x = info.x,
|
||||
.y = info.y,
|
||||
.width = info.width,
|
||||
.height = info.height,
|
||||
.pixel_width = info.pixel_width,
|
||||
.pixel_height = info.pixel_height,
|
||||
.display_scale = info.display_scale,
|
||||
};
|
||||
auto* owner = entry->owner;
|
||||
const auto callback = entry->value.onEvent;
|
||||
void* userData = entry->value.userData;
|
||||
try {
|
||||
callback(owner->context.get(), handle, &windowEvent, userData);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(
|
||||
*owner, MOD_ERROR, fmt::format("Exception in window event callback: {}", e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*owner, MOD_ERROR, "Unknown exception in window event callback");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ModResult window_acquire_for_graphics(
|
||||
LoadedMod& mod, WindowHandle handle, SDL_Window*& outWindow) {
|
||||
outWindow = nullptr;
|
||||
auto* slot = resolve_window(mod, handle);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (slot->graphicsRefs != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
++slot->graphicsRefs;
|
||||
outWindow = slot->window;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void window_release_for_graphics(LoadedMod& mod, WindowHandle handle) {
|
||||
auto* slot = resolve_window(mod, handle);
|
||||
if (slot != nullptr && slot->graphicsRefs > 0) {
|
||||
--slot->graphicsRefs;
|
||||
}
|
||||
}
|
||||
|
||||
bool window_get_pixel_size(LoadedMod& mod, WindowHandle handle, uint32_t& width, uint32_t& height) {
|
||||
auto* slot = resolve_window(mod, handle);
|
||||
if (slot == nullptr) {
|
||||
return false;
|
||||
}
|
||||
int pixelWidth = 0;
|
||||
int pixelHeight = 0;
|
||||
if (!SDL_GetWindowSizeInPixels(slot->window, &pixelWidth, &pixelHeight) || pixelWidth <= 0 ||
|
||||
pixelHeight <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
width = static_cast<uint32_t>(pixelWidth);
|
||||
height = static_cast<uint32_t>(pixelHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
constinit const ServiceModule g_windowModule{
|
||||
.id = WINDOW_SERVICE_ID,
|
||||
.majorVersion = WINDOW_SERVICE_MAJOR,
|
||||
.minorVersion = WINDOW_SERVICE_MINOR,
|
||||
.service = &s_windowService,
|
||||
.modDetached = remove_mod_windows,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
union SDL_Event;
|
||||
struct SDL_Window;
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
}
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
// Routes an SDL event for an auxiliary mod window.
|
||||
// Returns true when the event belongs to one.
|
||||
bool window_dispatch_event(const SDL_Event& event);
|
||||
ModResult window_acquire_for_graphics(LoadedMod& mod, WindowHandle handle, SDL_Window*& outWindow);
|
||||
void window_release_for_graphics(LoadedMod& mod, WindowHandle handle);
|
||||
bool window_get_pixel_size(LoadedMod& mod, WindowHandle handle, uint32_t& width, uint32_t& height);
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -71,6 +71,9 @@ Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
|
||||
}
|
||||
|
||||
auto* elem = append(parent, "toast");
|
||||
if (!toast.modId.empty()) {
|
||||
elem->SetAttribute("mod-id", toast.modId);
|
||||
}
|
||||
if (!toast.type.empty()) {
|
||||
elem->SetClass(toast.type, true);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ struct Toast {
|
||||
Rml::String title;
|
||||
Rml::String content;
|
||||
clock::duration duration;
|
||||
Rml::String modId;
|
||||
};
|
||||
|
||||
// Button clicked/pressed
|
||||
|
||||
@@ -57,13 +57,14 @@
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/game_clock.h"
|
||||
#include "dusk/gyro.h"
|
||||
#include "dusk/mouse.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/svc/window.hpp"
|
||||
#include "dusk/mouse.h"
|
||||
#include "dusk/os.h"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
#include "dusk/ui/overlay.hpp"
|
||||
@@ -157,6 +158,9 @@ bool launchUILoop() {
|
||||
while (event != nullptr && event->type != AURORA_NONE) {
|
||||
switch (event->type) {
|
||||
case AURORA_SDL_EVENT:
|
||||
if (dusk::mods::svc::window_dispatch_event(event->sdl)) {
|
||||
break;
|
||||
}
|
||||
dusk::mouse::handle_event(event->sdl);
|
||||
dusk::ui::handle_event(event->sdl);
|
||||
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
|
||||
@@ -244,6 +248,9 @@ void main01(void) {
|
||||
dusk::mouse::on_focus_gained();
|
||||
break;
|
||||
case AURORA_SDL_EVENT:
|
||||
if (dusk::mods::svc::window_dispatch_event(event->sdl)) {
|
||||
break;
|
||||
}
|
||||
dusk::mouse::handle_event(event->sdl);
|
||||
dusk::ui::handle_event(event->sdl);
|
||||
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
|
||||
|
||||
Reference in New Issue
Block a user