mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-30 07:34:37 -04:00
Mods: WindowService, log wrappers, external rendering (#2251)
This commit is contained in:
@@ -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)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
+67
-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`)
|
||||
|
||||
@@ -420,6 +433,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 +484,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 +534,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 +561,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 +572,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 +587,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 +603,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 +643,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: 81f12f31d2...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
|
||||
|
||||
+257
-32
@@ -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 {
|
||||
|
||||
@@ -71,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;
|
||||
@@ -188,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) {
|
||||
@@ -396,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*) {
|
||||
@@ -408,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;
|
||||
}
|
||||
@@ -441,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) {
|
||||
@@ -596,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;
|
||||
}
|
||||
|
||||
@@ -654,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)) {
|
||||
@@ -753,16 +930,27 @@ void render_shadow_map(
|
||||
// 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;
|
||||
@@ -774,6 +962,9 @@ void on_scene_after_opaque(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;
|
||||
}
|
||||
|
||||
@@ -807,15 +998,31 @@ void on_scene_after_opaque(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.
|
||||
@@ -905,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;
|
||||
}
|
||||
|
||||
@@ -941,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;
|
||||
}
|
||||
|
||||
@@ -1063,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;
|
||||
@@ -1090,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);
|
||||
@@ -1114,6 +1337,8 @@ MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
+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
-188
@@ -1,219 +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.
|
||||
*/
|
||||
#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; } \
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#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
|
||||
@@ -273,13 +277,4 @@ typedef struct UiService {
|
||||
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
|
||||
} 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
|
||||
|
||||
@@ -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
|
||||
@@ -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