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);
+8 -13
View File
@@ -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);
+242
View File
@@ -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
+5 -10
View File
@@ -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);
+5 -10
View File
@@ -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);
+46
View File
@@ -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
+6 -10
View File
@@ -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);
+6 -10
View File
@@ -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);
+8 -12
View File
@@ -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);
+5 -10
View File
@@ -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);
+103
View File
@@ -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
View File
@@ -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
+1
View File
@@ -209,6 +209,7 @@ void ModLoader::init_services() {
&svc::g_uiModule,
&svc::g_gameModule,
&svc::g_cameraModule,
&svc::g_windowModule,
&svc::g_gfxModule,
})
{
+1
View File
@@ -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
+352
View File
@@ -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
+21
View File
@@ -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
+9 -2
View File
@@ -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);