Mods: WindowService, log wrappers, external rendering (#2251)

This commit is contained in:
Luke Street
2026-07-30 00:24:24 -06:00
committed by GitHub
parent 48de4bcb08
commit 7305ef09b9
36 changed files with 2195 additions and 395 deletions
+1
View File
@@ -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)
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+1 -1
+2
View File
@@ -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
View File
@@ -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;
+20
View File
@@ -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
)
+7
View File
@@ -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."
}
+35
View File
@@ -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);
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
struct WindowEvent;
namespace window_demo {
void log_window_event(const WindowEvent* event);
} // namespace window_demo
+229
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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), \
+6 -10
View File
@@ -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);
+6 -10
View File
@@ -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);
+5 -10
View File
@@ -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
View File
@@ -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);

Some files were not shown because too many files have changed in this diff Show More