From 7305ef09b9a021565666b648b67cdcbe367b73f4 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 30 Jul 2026 00:24:24 -0600 Subject: [PATCH] Mods: WindowService, log wrappers, external rendering (#2251) --- CMakeLists.txt | 1 + cmake/GameABIConfig.cmake | 4 + cmake/ModSDK.cmake | 28 +- docs/modding.md | 78 +++- extern/aurora | 2 +- files.cmake | 2 + mods/shadow_mod/src/mod.cpp | 289 ++++++++++++-- mods/window_demo/CMakeLists.txt | 20 + mods/window_demo/mod.json | 7 + mods/window_demo/src/logging.cpp | 35 ++ mods/window_demo/src/logging.hpp | 9 + mods/window_demo/src/mod.cpp | 229 ++++++++++++ sdk/CMakeLists.txt | 2 +- sdk/include/mods/api.h | 18 +- sdk/include/mods/hook.hpp | 213 ++--------- sdk/include/mods/service.hpp | 4 +- sdk/include/mods/svc/camera.h | 16 +- sdk/include/mods/svc/config.h | 16 +- sdk/include/mods/svc/game.h | 15 +- sdk/include/mods/svc/gfx.h | 84 ++++- sdk/include/mods/svc/hook.h | 21 +- sdk/include/mods/svc/hook.hpp | 242 ++++++++++++ sdk/include/mods/svc/host.h | 15 +- sdk/include/mods/svc/log.h | 15 +- sdk/include/mods/svc/log.hpp | 46 +++ sdk/include/mods/svc/overlay.h | 16 +- sdk/include/mods/svc/resource.h | 16 +- sdk/include/mods/svc/texture.h | 20 +- sdk/include/mods/svc/ui.h | 15 +- sdk/include/mods/svc/window.h | 103 +++++ src/dusk/mods/svc/gfx.cpp | 623 +++++++++++++++++++++++++++++-- src/dusk/mods/svc/registry.cpp | 1 + src/dusk/mods/svc/registry.hpp | 1 + src/dusk/mods/svc/window.cpp | 352 +++++++++++++++++ src/dusk/mods/svc/window.hpp | 21 ++ src/m_Do/m_Do_main.cpp | 11 +- 36 files changed, 2195 insertions(+), 395 deletions(-) create mode 100644 mods/window_demo/CMakeLists.txt create mode 100644 mods/window_demo/mod.json create mode 100644 mods/window_demo/src/logging.cpp create mode 100644 mods/window_demo/src/logging.hpp create mode 100644 mods/window_demo/src/mod.cpp create mode 100644 sdk/include/mods/svc/hook.hpp create mode 100644 sdk/include/mods/svc/log.hpp create mode 100644 sdk/include/mods/svc/window.h create mode 100644 src/dusk/mods/svc/window.cpp create mode 100644 src/dusk/mods/svc/window.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b1c8986597..a4a3d2cc8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/cmake/GameABIConfig.cmake b/cmake/GameABIConfig.cmake index 0b965aa082..ef3b5818dc 100644 --- a/cmake/GameABIConfig.cmake +++ b/cmake/GameABIConfig.cmake @@ -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) diff --git a/cmake/ModSDK.cmake b/cmake/ModSDK.cmake index 0dfe5c48b4..da9aa51f38 100644 --- a/cmake/ModSDK.cmake +++ b/cmake/ModSDK.cmake @@ -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 () diff --git a/docs/modding.md b/docs/modding.md index 2600b4721d..80e1998ea5 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -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::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(svc_hook, on_pos_move_pre); +mods::hook::add_pre(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(svc_hook, on_pos_move_post); +mods::hook::add_post(on_pos_move_post); ``` ### Replace-hooks @@ -531,7 +587,7 @@ void on_execute_replace(ModContext*, void* args, void* retval, void*) { } } -mods::hook_replace(svc_hook, on_execute_replace); +mods::hook::replace(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(svc_hook, on_hookshot_hit_pre); +mods::hook::add_pre(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(svc_hook, on_create_item_pre); +mods::hook::add_pre(on_create_item_pre); ``` For reference parameters (e.g. `const cXyz& pos`), `arg_ref` yields a direct reference. diff --git a/extern/aurora b/extern/aurora index 81f12f31d2..0bddb86249 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 81f12f31d23ec822d8bde2031c91e94c470911eb +Subproject commit 0bddb8624905d74cc47202a71962f9c9834c0933 diff --git a/files.cmake b/files.cmake index fa96e313c6..7eb8ba6038 100644 --- a/files.cmake +++ b/files.cmake @@ -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 diff --git a/mods/shadow_mod/src/mod.cpp b/mods/shadow_mod/src/mod.cpp index 840ebdbd91..36f33de41d 100644 --- a/mods/shadow_mod/src/mod.cpp +++ b/mods/shadow_mod/src/mod.cpp @@ -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 #include @@ -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(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(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(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(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(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(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
Light View: " "renders the game world directly from the light camera
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(svc_hook, on_game_shadow_pre) != MOD_OK || - mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || - mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK) + if (mods::hook::add_pre(on_game_shadow_pre) != MOD_OK || + mods::hook::add_pre(on_game_shadow_pre) != MOD_OK || + mods::hook::add_pre(on_game_shadow_pre) != MOD_OK) { return mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering"); } - if (mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK || - mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK) + if (mods::hook::add_pre(on_frustum_clip_pre) != MOD_OK || + mods::hook::add_pre(on_frustum_clip_pre) != MOD_OK) { return mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping"); } - if (mods::hook_add_pre(svc_hook, on_copy_tex_pre) != MOD_OK) { + if (mods::hook::add_pre(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; diff --git a/mods/window_demo/CMakeLists.txt b/mods/window_demo/CMakeLists.txt new file mode 100644 index 0000000000..9150ec7de6 --- /dev/null +++ b/mods/window_demo/CMakeLists.txt @@ -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 +) diff --git a/mods/window_demo/mod.json b/mods/window_demo/mod.json new file mode 100644 index 0000000000..ff79e62431 --- /dev/null +++ b/mods/window_demo/mod.json @@ -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." +} diff --git a/mods/window_demo/src/logging.cpp b/mods/window_demo/src/logging.cpp new file mode 100644 index 0000000000..87015c713a --- /dev/null +++ b/mods/window_demo/src/logging.cpp @@ -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); +} diff --git a/mods/window_demo/src/logging.hpp b/mods/window_demo/src/logging.hpp new file mode 100644 index 0000000000..3f62800d86 --- /dev/null +++ b/mods/window_demo/src/logging.hpp @@ -0,0 +1,9 @@ +#pragma once + +struct WindowEvent; + +namespace window_demo { + +void log_window_event(const WindowEvent* event); + +} // namespace window_demo diff --git a/mods/window_demo/src/mod.cpp b/mods/window_demo/src/mod.cpp new file mode 100644 index 0000000000..fb3cc31809 --- /dev/null +++ b/mods/window_demo/src/mod.cpp @@ -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 +#include +#include + +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(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; +} +} diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt index cd80781eb6..29f1d94a9a 100644 --- a/sdk/CMakeLists.txt +++ b/sdk/CMakeLists.txt @@ -5,7 +5,7 @@ # # Usage (from a mod project): # add_subdirectory(/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. diff --git a/sdk/include/mods/api.h b/sdk/include/mods/api.h index 3a72135388..3fde6c9f6a 100644 --- a/sdk/include/mods/api.h +++ b/sdk/include/mods/api.h @@ -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 { \ + 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 diff --git a/sdk/include/mods/hook.hpp b/sdk/include/mods/hook.hpp index a8d91ab68c..6087ff49bb 100644 --- a/sdk/include/mods/hook.hpp +++ b/sdk/include/mods/hook.hpp @@ -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: is deprecated; include instead") +#else +#warning " is deprecated; include instead" #endif -#include - -#include -#include +#include namespace mods { -template -T arg(void* argsRaw, int n) noexcept { - void** args = static_cast(argsRaw); - return *static_cast>>(args[n]); -} - -template -std::remove_reference_t& arg_ref(void* argsRaw, int n) noexcept { - void** args = static_cast(argsRaw); - return *static_cast>>(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 -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) { - 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(std::addressof(result))); - if (!skipOriginal) { - result = g_orig(args...); - } - dispatch_post(nullptr, static_cast(std::addressof(result))); - return result; - } - } else { - void* ptrs[] = {static_cast(std::addressof(args))...}; - if constexpr (std::is_void_v) { - const bool skipOriginal = dispatch_pre(static_cast(ptrs), nullptr); - if (!skipOriginal) { - g_orig(args...); - } - dispatch_post(static_cast(ptrs), nullptr); - } else { - R result{}; - const bool skipOriginal = dispatch_pre( - static_cast(ptrs), static_cast(std::addressof(result))); - if (!skipOriginal) { - result = g_orig(args...); - } - dispatch_post(static_cast(ptrs), static_cast(std::addressof(result))); - return result; - } - } - } -}; - -namespace detail { -template -using TargetTag = std::integral_constant; -template -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 -struct Hook; - -template -struct Hook : HookImpl, R, C*, A...> {}; - -template -struct Hook : HookImpl, R, const C*, A...> {}; - -template -struct Hook : HookImpl, 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 -struct NamedHook; - -template -struct NamedHook : HookImpl, 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(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 ModResult hook_install(const HookService* hooks) { - if (hooks == nullptr) { - return MOD_UNAVAILABLE; - } + return hook::install(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(Entry::trampoline), - reinterpret_cast(&Entry::g_orig)); +template +ModResult hook_install() { + return hook::install(); } template ModResult hook_add_pre( const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { - const ModResult installed = hook_install(hooks); - if (installed != MOD_OK) { - return installed; - } + return hook::add_pre(hooks, callback, options); +} - return hooks->add_pre(mod_ctx, Entry::target, callback, options); +template +ModResult hook_add_pre(HookPreFn callback, const HookOptions* options = nullptr) { + return hook::add_pre(callback, options); } template ModResult hook_add_post( const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { - const ModResult installed = hook_install(hooks); - if (installed != MOD_OK) { - return installed; - } + return hook::add_post(hooks, callback, options); +} - return hooks->add_post(mod_ctx, Entry::target, callback, options); +template +ModResult hook_add_post(HookPostFn callback, const HookOptions* options = nullptr) { + return hook::add_post(callback, options); } template ModResult hook_replace( const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { - const ModResult installed = hook_install(hooks); - if (installed != MOD_OK) { - return installed; - } + return hook::replace(hooks, callback, options); +} - return hooks->replace(mod_ctx, Entry::target, callback, options); +template +ModResult hook_replace(HookReplaceFn callback, const HookOptions* options = nullptr) { + return hook::replace(callback, options); } } // namespace mods diff --git a/sdk/include/mods/service.hpp b/sdk/include/mods/service.hpp index a6010a375c..ed734ffc15 100644 --- a/sdk/include/mods/service.hpp +++ b/sdk/include/mods/service.hpp @@ -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(flags_value)}, \ static_cast(major_value), \ diff --git a/sdk/include/mods/svc/camera.h b/sdk/include/mods/svc/camera.h index f6de5b8146..8934340cd6 100644 --- a/sdk/include/mods/svc/camera.h +++ b/sdk/include/mods/svc/camera.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/config.h b/sdk/include/mods/svc/config.h index cd4afdbbdf..fb04f6e9ea 100644 --- a/sdk/include/mods/svc/config.h +++ b/sdk/include/mods/svc/config.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/game.h b/sdk/include/mods/svc/game.h index 0a3585b912..ecb8bcfeb7 100644 --- a/sdk/include/mods/svc/game.h +++ b/sdk/include/mods/svc/game.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +#include +#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 - -template <> -struct mods::ServiceTraits { - 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); diff --git a/sdk/include/mods/svc/gfx.h b/sdk/include/mods/svc/gfx.h index 03b24ed986..5041c611e3 100644 --- a/sdk/include/mods/svc/gfx.h +++ b/sdk/include/mods/svc/gfx.h @@ -1,6 +1,11 @@ #pragma once #include +#include + +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/hook.h b/sdk/include/mods/svc/hook.h index 3d4e9c0472..e9c64b1ca4 100644 --- a/sdk/include/mods/svc/hook.h +++ b/sdk/include/mods/svc/hook.h @@ -2,9 +2,13 @@ #include +#ifdef __cplusplus +#include +#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 / arg_ref from mods/hook.hpp. `userdata` is the pointer + * them through mods::arg / arg_ref 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 { - 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); diff --git a/sdk/include/mods/svc/hook.hpp b/sdk/include/mods/svc/hook.hpp new file mode 100644 index 0000000000..e61562da36 --- /dev/null +++ b/sdk/include/mods/svc/hook.hpp @@ -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 + +#include +#include + +namespace mods { + +template +T arg(void* argsRaw, int n) noexcept { + void** args = static_cast(argsRaw); + return *static_cast>>(args[n]); +} + +template +std::remove_reference_t& arg_ref(void* argsRaw, int n) noexcept { + void** args = static_cast(argsRaw); + return *static_cast>>(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 +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) { + 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(std::addressof(result))); + if (!skipOriginal) { + result = g_orig(args...); + } + dispatch_post(nullptr, static_cast(std::addressof(result))); + return result; + } + } else { + void* ptrs[] = {static_cast(std::addressof(args))...}; + if constexpr (std::is_void_v) { + const bool skipOriginal = dispatch_pre(static_cast(ptrs), nullptr); + if (!skipOriginal) { + g_orig(args...); + } + dispatch_post(static_cast(ptrs), nullptr); + } else { + R result{}; + const bool skipOriginal = dispatch_pre( + static_cast(ptrs), static_cast(std::addressof(result))); + if (!skipOriginal) { + result = g_orig(args...); + } + dispatch_post(static_cast(ptrs), static_cast(std::addressof(result))); + return result; + } + } + } +}; + +namespace detail { +template +using TargetTag = std::integral_constant; +template +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 +struct Hook; + +template +struct Hook : HookImpl, R, C*, A...> {}; + +template +struct Hook : HookImpl, R, const C*, A...> {}; + +template +struct Hook : HookImpl, 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 +struct NamedHook; + +template +struct NamedHook : HookImpl, 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(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 +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(Entry::trampoline), + reinterpret_cast(&Entry::g_orig)); +} + +template +ModResult install() { + return install(svc_hook); +} + +template +ModResult add_pre( + const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { + const ModResult installed = install(hooks); + if (installed != MOD_OK) { + return installed; + } + + return hooks->add_pre(mod_ctx, Entry::target, callback, options); +} + +template +ModResult add_pre(HookPreFn callback, const HookOptions* options = nullptr) { + return add_pre(svc_hook, callback, options); +} + +template +ModResult add_post( + const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { + const ModResult installed = install(hooks); + if (installed != MOD_OK) { + return installed; + } + + return hooks->add_post(mod_ctx, Entry::target, callback, options); +} + +template +ModResult add_post(HookPostFn callback, const HookOptions* options = nullptr) { + return add_post(svc_hook, callback, options); +} + +template +ModResult replace( + const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { + const ModResult installed = install(hooks); + if (installed != MOD_OK) { + return installed; + } + + return hooks->replace(mod_ctx, Entry::target, callback, options); +} + +template +ModResult replace(HookReplaceFn callback, const HookOptions* options = nullptr) { + return replace(svc_hook, callback, options); +} + +} // namespace hook +} // namespace mods diff --git a/sdk/include/mods/svc/host.h b/sdk/include/mods/svc/host.h index 9e278d5790..24265cab99 100644 --- a/sdk/include/mods/svc/host.h +++ b/sdk/include/mods/svc/host.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/log.h b/sdk/include/mods/svc/log.h index 0bb9ca74ed..1040710492 100644 --- a/sdk/include/mods/svc/log.h +++ b/sdk/include/mods/svc/log.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/log.hpp b/sdk/include/mods/svc/log.hpp new file mode 100644 index 0000000000..372cf06d8d --- /dev/null +++ b/sdk/include/mods/svc/log.hpp @@ -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 + +#include + +#include + +namespace mods::log { + +template +void write(LogLevel level, fmt::format_string formatString, Args&&... args) { + const auto message = fmt::format(formatString, std::forward(args)...); + svc_log->write(mod_ctx, level, message.c_str()); +} + +template +void trace(fmt::format_string formatString, Args&&... args) { + write(LOG_LEVEL_TRACE, formatString, std::forward(args)...); +} + +template +void debug(fmt::format_string formatString, Args&&... args) { + write(LOG_LEVEL_DEBUG, formatString, std::forward(args)...); +} + +template +void info(fmt::format_string formatString, Args&&... args) { + write(LOG_LEVEL_INFO, formatString, std::forward(args)...); +} + +template +void warn(fmt::format_string formatString, Args&&... args) { + write(LOG_LEVEL_WARN, formatString, std::forward(args)...); +} + +template +void error(fmt::format_string formatString, Args&&... args) { + write(LOG_LEVEL_ERROR, formatString, std::forward(args)...); +} + +} // namespace mods::log diff --git a/sdk/include/mods/svc/overlay.h b/sdk/include/mods/svc/overlay.h index 76e24c878b..ea46bc8798 100644 --- a/sdk/include/mods/svc/overlay.h +++ b/sdk/include/mods/svc/overlay.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/resource.h b/sdk/include/mods/svc/resource.h index 05a22754a3..57fddc9778 100644 --- a/sdk/include/mods/svc/resource.h +++ b/sdk/include/mods/svc/resource.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/texture.h b/sdk/include/mods/svc/texture.h index cdcfbe7e2a..dac73fbade 100644 --- a/sdk/include/mods/svc/texture.h +++ b/sdk/include/mods/svc/texture.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index e19ec65f6c..1635a96c5f 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -3,6 +3,10 @@ #include #include +#ifdef __cplusplus +#include +#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 { - 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); diff --git a/sdk/include/mods/svc/window.h b/sdk/include/mods/svc/window.h new file mode 100644 index 0000000000..7cee8a5abe --- /dev/null +++ b/sdk/include/mods/svc/window.h @@ -0,0 +1,103 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#include + +#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); diff --git a/src/dusk/mods/svc/gfx.cpp b/src/dusk/mods/svc/gfx.cpp index e14b587f5e..f522555e66 100644 --- a/src/dusk/mods/svc/gfx.cpp +++ b/src/dusk/mods/svc/gfx.cpp @@ -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 +#include #include +#include +#include #include #include +#include #include +#include #include #include #include @@ -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 status = PresentTargetStatus::Pending; + bool configured = false; + bool presentPending = false; + bool vsync = true; +}; + +bool present_target_failed(const std::shared_ptr& 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 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 drawIds; - std::vector 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& drawIds, +void collect_mod_types_locked(LoadedMod& owner, std::vector& drawIds, std::vector& 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& 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 +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& 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(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(preferredAlphaMode); + if (alphaMode != wgpu::CompositeAlphaMode::Auto && + !contains(capabilities.alphaModes, capabilities.alphaModeCount, alphaMode)) + { + alphaMode = capabilities.alphaModes[0]; + } + + auto usage = static_cast(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(reinterpret_cast(userdata)); + GfxPresentFn fn = nullptr; + void* userData = nullptr; + ModContext* modContext = nullptr; + std::string ownerId; + std::shared_ptr 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(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(reinterpret_cast(userdata)); + std::shared_ptr 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(); + 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(static_cast(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 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 drawIds; + std::vector 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 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 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 drawIds; - std::vector 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(aurora::gfx::color_format()); outInfo->depth_format = static_cast(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(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 diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 2033d47475..622db55da9 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -209,6 +209,7 @@ void ModLoader::init_services() { &svc::g_uiModule, &svc::g_gameModule, &svc::g_cameraModule, + &svc::g_windowModule, &svc::g_gfxModule, }) { diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 191626e0ef..4ba4f695ab 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -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 diff --git a/src/dusk/mods/svc/window.cpp b/src/dusk/mods/svc/window.cpp new file mode 100644 index 0000000000..ddc5832843 --- /dev/null +++ b/src/dusk/mods/svc/window.cpp @@ -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 +#include +#include +#include +#include + +#include +#include +#include + +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 s_windows; +std::unordered_map 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(width); + info.height = static_cast(height); + info.pixel_width = static_cast(pixelWidth); + info.pixel_height = static_cast(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(width), static_cast(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(pixelWidth); + height = static_cast(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 diff --git a/src/dusk/mods/svc/window.hpp b/src/dusk/mods/svc/window.hpp new file mode 100644 index 0000000000..29b48c1b75 --- /dev/null +++ b/src/dusk/mods/svc/window.hpp @@ -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 diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index d291043d67..89623161c2 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -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);