mirror of
https://github.com/TwilitRealm/dusklight.git
synced 2026-09-12 21:29:43 -07:00
Mods: WindowService, log wrappers, external rendering (#2251)
This commit is contained in:
@@ -610,6 +610,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR
|
||||
add_subdirectory(mods/template_mod)
|
||||
add_subdirectory(mods/ao_mod)
|
||||
add_subdirectory(mods/shadow_mod)
|
||||
add_subdirectory(mods/window_demo)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
|
||||
@@ -62,3 +62,7 @@ target_sources(dusklight_mod_feature_game INTERFACE
|
||||
add_library(dusklight_mod_feature_webgpu INTERFACE)
|
||||
target_link_libraries(dusklight_mod_feature_webgpu INTERFACE dusklight_mod_api)
|
||||
target_compile_definitions(dusklight_mod_feature_webgpu INTERFACE DUSK_MOD_FEATURE_WEBGPU=1)
|
||||
|
||||
add_library(dusklight_mod_feature_fmt INTERFACE)
|
||||
target_link_libraries(dusklight_mod_feature_fmt INTERFACE dusklight_mod_api)
|
||||
target_compile_definitions(dusklight_mod_feature_fmt INTERFACE DUSK_MOD_FEATURE_FMT=1)
|
||||
|
||||
+27
-1
@@ -113,6 +113,29 @@ function(_mod_add_webgpu_headers target_name)
|
||||
endif ()
|
||||
endfunction()
|
||||
|
||||
function(_mod_add_fmt target_name)
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
find_package(fmt 11 CONFIG QUIET GLOBAL)
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
include(FetchContent)
|
||||
message(STATUS "Mod SDK: fetching fmt")
|
||||
# Keep the fallback version in sync with extern/aurora/extern/CMakeLists.txt.
|
||||
FetchContent_Declare(fmt
|
||||
URL https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz
|
||||
URL_HASH SHA256=ea7de4299689e12b6dddd392f9896f08fb0777ac7168897a244a6d6085043fea
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL)
|
||||
FetchContent_MakeAvailable(fmt)
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
message(FATAL_ERROR "add_mod: FEATURES fmt could not provide fmt::fmt-header-only")
|
||||
endif ()
|
||||
target_link_libraries(${target_name} PRIVATE fmt::fmt-header-only)
|
||||
endfunction()
|
||||
|
||||
function(add_mod target_name)
|
||||
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR"
|
||||
"SOURCES;RUNTIME_LIBRARIES;FEATURES" ${ARGN})
|
||||
@@ -127,7 +150,7 @@ function(add_mod target_name)
|
||||
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
|
||||
endif ()
|
||||
|
||||
set(_supported_features game webgpu)
|
||||
set(_supported_features fmt game webgpu)
|
||||
set(_features "")
|
||||
foreach (_feature IN LISTS ARG_FEATURES)
|
||||
list(FIND _supported_features "${_feature}" _feature_index)
|
||||
@@ -167,6 +190,9 @@ function(add_mod target_name)
|
||||
if (_feature STREQUAL "webgpu")
|
||||
_mod_add_webgpu_headers(${target_name})
|
||||
endif ()
|
||||
if (_feature STREQUAL "fmt")
|
||||
_mod_add_fmt(${target_name})
|
||||
endif ()
|
||||
if (_feature STREQUAL "game" OR _feature STREQUAL "webgpu")
|
||||
set(_needs_host_link TRUE)
|
||||
endif ()
|
||||
|
||||
+67
-11
@@ -54,7 +54,7 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchDusklight.cmake")
|
||||
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
|
||||
|
||||
add_mod(my_mod
|
||||
FEATURES game # remove for service/asset-only mods; add webgpu for GfxService
|
||||
FEATURES game fmt # remove game for service-only mods; add webgpu for GfxService
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res # mod resources, including icon.png and banner.png
|
||||
@@ -64,6 +64,8 @@ add_mod(my_mod
|
||||
```
|
||||
|
||||
Available features:
|
||||
|
||||
- `fmt`: Provides the header-only `{fmt}` library and the formatted logging helpers in `mods/svc/log.hpp`.
|
||||
- `game`: Allows calling into and hooking game code. Mods that **only** use services may omit it, providing a wider
|
||||
range of compatibility with Dusklight versions and a slightly faster build process.
|
||||
- `webgpu`: Allows importing the WebGPU API (`webgpu/webgpu.h`). Must be enabled when using
|
||||
@@ -143,6 +145,9 @@ IMPORT_SERVICE_VERSION(LogService, svc_log, 0); // required, minimum minor ver
|
||||
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
|
||||
```
|
||||
|
||||
A service must be imported in only **one** file (usually your `mod.cpp`). Other files may simply use `svc_log` or
|
||||
`mods::log::` after including the appropriate header.
|
||||
|
||||
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
|
||||
allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major**
|
||||
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
|
||||
@@ -179,7 +184,15 @@ svc_log->write(mod_ctx, LOG_LEVEL_DEBUG, "verbose details");
|
||||
```
|
||||
|
||||
Messages appear in the console prefixed with your mod ID. Messages are plain UTF-8 strings and are copied before the
|
||||
call returns; use `snprintf` or `fmt::format` for formatting.
|
||||
call returns. C++ mods can enable `add_mod(... FEATURES fmt)` and use the formatted logging helpers in
|
||||
`mods/svc/log.hpp`:
|
||||
|
||||
```cpp
|
||||
#include <mods/svc/log.hpp>
|
||||
|
||||
mods::log::info("spawned actor {} at ({}, {})", actorName, x, y);
|
||||
mods::log::warn("health is down to {:.1f}%", healthPercent);
|
||||
```
|
||||
|
||||
### ResourceService (`mods/svc/resource.h`)
|
||||
|
||||
@@ -236,7 +249,7 @@ every service dropped its state. For your own mod's teardown, use `mod_shutdown`
|
||||
### HookService (`mods/svc/hook.h`)
|
||||
|
||||
Installs hooks on game functions and resolves symbols by name. You'll rarely call it directly; use the typed helpers in
|
||||
`mods/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||
`mods/svc/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||
|
||||
### OverlayService (`mods/svc/overlay.h`)
|
||||
|
||||
@@ -420,6 +433,26 @@ existing documents restyle immediately, and future ones pick it up when created.
|
||||
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
|
||||
unless changing host UI is intentional.
|
||||
|
||||
### WindowService (`mods/svc/window.h`)
|
||||
|
||||
Allows creating new windows that can be rendered to via `GfxService`.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
|
||||
WindowDesc desc = WINDOW_DESC_INIT;
|
||||
desc.title = "My auxiliary view";
|
||||
desc.on_event = on_window_event;
|
||||
WindowHandle window = 0;
|
||||
svc_window->create_window(mod_ctx, &desc, &window);
|
||||
```
|
||||
|
||||
Window callbacks run on the game thread. A close event is only a request; call `destroy_window` when the mod is ready to
|
||||
close it. A window attached to a GfxService present target cannot be destroyed until that target is unregistered. Only
|
||||
one present target may be attached to a WindowService window at a time.
|
||||
|
||||
New windows are hidden by default so a mod can finish attaching graphics before calling `show_window`.
|
||||
|
||||
### GfxService (`mods/svc/gfx.h`)
|
||||
|
||||
**Requires `add_mod(... FEATURES webgpu)`**
|
||||
@@ -451,6 +484,30 @@ registered with `register_compute_type` follow the same worker-thread rule and r
|
||||
All WGPU handles from the service are borrowed. Resolved target views are valid for the current frame only. GPU objects
|
||||
created by a mod are owned by that mod and should be released in `mod_shutdown`.
|
||||
|
||||
#### External presentation
|
||||
|
||||
GfxService supports external presentation ("present targets") backed by either a WindowService window (via
|
||||
`register_window_present_target`) or a plain `WGPUSurface` (via `register_present_target`).
|
||||
|
||||
```cpp
|
||||
GfxPresentTargetDesc target_desc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
target_desc.render = render_auxiliary_view;
|
||||
GfxPresentTargetHandle target = 0;
|
||||
svc_gfx->register_window_present_target(mod_ctx, window, &target_desc, &target);
|
||||
|
||||
// From a stage callback:
|
||||
svc_gfx->push_present(mod_ctx, target, &payload, sizeof(payload));
|
||||
```
|
||||
|
||||
For WindowService windows, the surface is automatically reconfigured on window size changes.
|
||||
For plain `WBPUSurface`s, `resize_present_target` must be used to resize.
|
||||
|
||||
To create a `WGPUSurface` manually, `GfxDeviceInfo` holds the `WGPUInstance` and `WGPUAdapter` which can be used with
|
||||
`wgpuInstanceCreateSurface` and a chained `WGPUSurfaceSource*` struct.
|
||||
|
||||
`push_present` must be called every frame from a GfxService stage callback. If surface was lost, `push_present` returns
|
||||
`MOD_ERROR`. Unregister and re-register the target before trying again.
|
||||
|
||||
### CameraService (`mods/svc/camera.h`)
|
||||
|
||||
Converts a game view provided by a render callback into WebGPU-convention camera data. Matrix fields are column-major
|
||||
@@ -477,11 +534,10 @@ first in-game frame. Projection matrices match the renderer's WebGPU clip conven
|
||||
**Requires `add_mod(... FEATURES game)`**
|
||||
|
||||
Mods may hook the vast majority of game functions, including file-local static, private and virtual functions.
|
||||
`mods/hook.hpp` provides typed helpers over the hook service:
|
||||
`mods/svc/hook.hpp` provides typed helpers over the hook service:
|
||||
|
||||
```cpp
|
||||
#include "mods/hook.hpp"
|
||||
#include "mods/svc/hook.h"
|
||||
#include "mods/svc/hook.hpp"
|
||||
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
|
||||
@@ -505,7 +561,7 @@ HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
mods::hook_add_pre<LinkPosMove>(svc_hook, on_pos_move_pre);
|
||||
mods::hook::add_pre<LinkPosMove>(on_pos_move_pre);
|
||||
```
|
||||
|
||||
### Post-hooks
|
||||
@@ -516,7 +572,7 @@ if any.
|
||||
```cpp
|
||||
void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... }
|
||||
|
||||
mods::hook_add_post<LinkPosMove>(svc_hook, on_pos_move_post);
|
||||
mods::hook::add_post<LinkPosMove>(on_pos_move_post);
|
||||
```
|
||||
|
||||
### Replace-hooks
|
||||
@@ -531,7 +587,7 @@ void on_execute_replace(ModContext*, void* args, void* retval, void*) {
|
||||
}
|
||||
}
|
||||
|
||||
mods::hook_replace<LinkExecute>(svc_hook, on_execute_replace);
|
||||
mods::hook::replace<LinkExecute>(on_execute_replace);
|
||||
```
|
||||
|
||||
By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`,
|
||||
@@ -547,7 +603,7 @@ symbol name instead. You must supply the signature along with the name.
|
||||
DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
|
||||
void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
|
||||
|
||||
mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit_pre);
|
||||
mods::hook::add_pre<HookshotHit>(on_hookshot_hit_pre);
|
||||
...
|
||||
HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the original
|
||||
```
|
||||
@@ -587,7 +643,7 @@ HookAction on_create_item_pre(ModContext*, void* args, void*, void*) {
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
mods::hook_add_pre<CreateItem>(svc_hook, on_create_item_pre);
|
||||
mods::hook::add_pre<CreateItem>(on_create_item_pre);
|
||||
```
|
||||
|
||||
For reference parameters (e.g. `const cXyz& pos`), `arg_ref<cXyz>` yields a direct reference.
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 81f12f31d2...0bddb86249
@@ -1500,6 +1500,8 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/texture.cpp
|
||||
src/dusk/mods/svc/ui.cpp
|
||||
src/dusk/mods/svc/ui.hpp
|
||||
src/dusk/mods/svc/window.cpp
|
||||
src/dusk/mods/svc/window.hpp
|
||||
src/dusk/mouse.cpp
|
||||
src/dusk/scope_guard.hpp
|
||||
src/dusk/settings.cpp
|
||||
|
||||
+257
-32
@@ -20,7 +20,7 @@
|
||||
#include "dolphin/gx/GXPixel.h"
|
||||
#include "dolphin/gx/GXTransform.h"
|
||||
#include "m_Do/m_Do_mtx.h"
|
||||
#include "mods/hook.hpp"
|
||||
#include "mods/svc/hook.hpp"
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/camera.h"
|
||||
#include "mods/svc/config.h"
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "mods/svc/log.h"
|
||||
#include "mods/svc/resource.h"
|
||||
#include "mods/svc/ui.h"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -45,6 +46,7 @@ IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
IMPORT_SERVICE(CameraService, svc_camera);
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -71,6 +73,11 @@ WGPURenderPipeline g_compositePipeline = nullptr; // multiply blend
|
||||
WGPURenderPipeline g_compositeDebugPipeline = nullptr; // no blend (debug views)
|
||||
WGPUBindGroupLayout g_compositeLayout = nullptr;
|
||||
WGPUBindGroupLayout g_compositeDebugLayout = nullptr;
|
||||
WGPURenderPipeline g_debugPresentPipeline = nullptr;
|
||||
WGPUBindGroupLayout g_debugPresentLayout = nullptr;
|
||||
WGPUTextureFormat g_debugPresentFormat = WGPUTextureFormat_Undefined;
|
||||
WindowHandle g_debugWindow = 0;
|
||||
GfxPresentTargetHandle g_debugPresentTarget = 0;
|
||||
|
||||
struct MapPassOutput {
|
||||
bool ready = false;
|
||||
@@ -188,6 +195,34 @@ int64_t get_debug_mode() {
|
||||
return std::clamp<int64_t>(get_int_option(g_cvarDebugView, 0), 0, 10);
|
||||
}
|
||||
|
||||
bool debug_window_open() {
|
||||
return g_debugWindow != 0 && g_debugPresentTarget != 0;
|
||||
}
|
||||
|
||||
ModResult close_debug_window() {
|
||||
if (g_debugPresentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_debugPresentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_debugPresentTarget = 0;
|
||||
}
|
||||
if (g_debugWindow != 0) {
|
||||
const auto result = svc_window->destroy_window(mod_ctx, g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_debugWindow = 0;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void on_debug_window_event(ModContext*, WindowHandle, const WindowEvent* event, void*) {
|
||||
if (event->type == WINDOW_EVENT_CLOSE_REQUESTED && close_debug_window() != MOD_OK) {
|
||||
svc_log->error(mod_ctx, "failed to close shadow debug window");
|
||||
}
|
||||
}
|
||||
|
||||
bool matrix_ready(const Mtx m) {
|
||||
float basis = 0.0f;
|
||||
for (int r = 0; r < 3; ++r) {
|
||||
@@ -396,6 +431,90 @@ bool build_composite_pipeline(
|
||||
return outLayout != nullptr;
|
||||
}
|
||||
|
||||
void release_debug_present_pipeline() {
|
||||
if (g_debugPresentPipeline != nullptr) {
|
||||
wgpuRenderPipelineRelease(g_debugPresentPipeline);
|
||||
g_debugPresentPipeline = nullptr;
|
||||
}
|
||||
if (g_debugPresentLayout != nullptr) {
|
||||
wgpuBindGroupLayoutRelease(g_debugPresentLayout);
|
||||
g_debugPresentLayout = nullptr;
|
||||
}
|
||||
g_debugPresentFormat = WGPUTextureFormat_Undefined;
|
||||
}
|
||||
|
||||
bool ensure_debug_present_pipeline(const GfxPresentContext& ctx) {
|
||||
if (g_debugPresentPipeline != nullptr && g_debugPresentFormat == ctx.target_format) {
|
||||
return true;
|
||||
}
|
||||
release_debug_present_pipeline();
|
||||
|
||||
WGPUShaderSourceWGSL wgsl = WGPU_SHADER_SOURCE_WGSL_INIT;
|
||||
wgsl.code = {static_cast<const char*>(g_shaderSource.data), g_shaderSource.size};
|
||||
WGPUShaderModuleDescriptor moduleDesc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT;
|
||||
moduleDesc.nextInChain = &wgsl.chain;
|
||||
moduleDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
WGPUShaderModule module = wgpuDeviceCreateShaderModule(ctx.device, &moduleDesc);
|
||||
if (module == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT;
|
||||
colorTarget.format = ctx.target_format;
|
||||
WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT;
|
||||
fragment.module = module;
|
||||
fragment.entryPoint = {"fs_main", WGPU_STRLEN};
|
||||
fragment.targetCount = 1;
|
||||
fragment.targets = &colorTarget;
|
||||
|
||||
WGPURenderPipelineDescriptor pipelineDesc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT;
|
||||
pipelineDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
pipelineDesc.vertex.module = module;
|
||||
pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN};
|
||||
pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
pipelineDesc.multisample.count = 1;
|
||||
pipelineDesc.fragment = &fragment;
|
||||
g_debugPresentPipeline = wgpuDeviceCreateRenderPipeline(ctx.device, &pipelineDesc);
|
||||
wgpuShaderModuleRelease(module);
|
||||
if (g_debugPresentPipeline == nullptr) {
|
||||
return false;
|
||||
}
|
||||
g_debugPresentLayout = wgpuRenderPipelineGetBindGroupLayout(g_debugPresentPipeline, 0);
|
||||
if (g_debugPresentLayout == nullptr) {
|
||||
release_debug_present_pipeline();
|
||||
return false;
|
||||
}
|
||||
g_debugPresentFormat = ctx.target_format;
|
||||
return true;
|
||||
}
|
||||
|
||||
WGPUBindGroup create_composite_bind_group(WGPUDevice device, WGPUBindGroupLayout layout,
|
||||
WGPUBuffer uniformBuffer, const DrawPayload& data) {
|
||||
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
|
||||
layout == nullptr || uniformBuffer == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
|
||||
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
|
||||
entries[0].binding = 0;
|
||||
entries[0].textureView = data.sceneDepth;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = data.shadowMap;
|
||||
entries[2].binding = 2;
|
||||
entries[2].buffer = uniformBuffer;
|
||||
entries[2].offset = data.uniform_offset;
|
||||
entries[2].size = data.uniform_size;
|
||||
entries[3].binding = 3;
|
||||
entries[3].textureView = data.lightColor;
|
||||
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
|
||||
bindGroupDesc.layout = layout;
|
||||
bindGroupDesc.entryCount = 4;
|
||||
bindGroupDesc.entries = entries;
|
||||
return wgpuDeviceCreateBindGroup(device, &bindGroupDesc);
|
||||
}
|
||||
|
||||
// Render worker thread: fullscreen deferred-shadow composite.
|
||||
void on_draw(
|
||||
ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
@@ -408,29 +527,12 @@ void on_draw(
|
||||
WGPURenderPipeline pipeline =
|
||||
data.debug_mode != 0 ? g_compositeDebugPipeline : g_compositePipeline;
|
||||
WGPUBindGroupLayout layout = data.debug_mode != 0 ? g_compositeDebugLayout : g_compositeLayout;
|
||||
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
|
||||
pipeline == nullptr)
|
||||
{
|
||||
if (pipeline == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
|
||||
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
|
||||
entries[0].binding = 0;
|
||||
entries[0].textureView = data.sceneDepth;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = data.shadowMap;
|
||||
entries[2].binding = 2;
|
||||
entries[2].buffer = ctx->uniform_buffer;
|
||||
entries[2].offset = data.uniform_offset;
|
||||
entries[2].size = data.uniform_size;
|
||||
entries[3].binding = 3;
|
||||
entries[3].textureView = data.lightColor;
|
||||
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
|
||||
bindGroupDesc.layout = layout;
|
||||
bindGroupDesc.entryCount = 4;
|
||||
bindGroupDesc.entries = entries;
|
||||
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
|
||||
WGPUBindGroup bindGroup =
|
||||
create_composite_bind_group(ctx->device, layout, ctx->uniform_buffer, data);
|
||||
if (bindGroup == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -441,6 +543,81 @@ void on_draw(
|
||||
wgpuBindGroupRelease(bindGroup);
|
||||
}
|
||||
|
||||
// Render worker thread: draw the selected diagnostic into the auxiliary surface.
|
||||
void on_debug_present(
|
||||
ModContext*, const GfxPresentContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
WGPUBindGroup bindGroup = nullptr;
|
||||
if (payloadSize == sizeof(DrawPayload)) {
|
||||
DrawPayload data;
|
||||
std::memcpy(&data, payload, sizeof(data));
|
||||
if (ensure_debug_present_pipeline(*ctx)) {
|
||||
bindGroup = create_composite_bind_group(
|
||||
ctx->device, g_debugPresentLayout, ctx->uniform_buffer, data);
|
||||
}
|
||||
}
|
||||
|
||||
WGPURenderPassColorAttachment colorAttachment = WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT;
|
||||
colorAttachment.view = ctx->target_view;
|
||||
colorAttachment.loadOp = WGPULoadOp_Clear;
|
||||
colorAttachment.storeOp = WGPUStoreOp_Store;
|
||||
colorAttachment.clearValue = WGPUColor{0.0, 0.0, 0.0, 1.0};
|
||||
WGPURenderPassDescriptor passDesc = WGPU_RENDER_PASS_DESCRIPTOR_INIT;
|
||||
passDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
passDesc.colorAttachmentCount = 1;
|
||||
passDesc.colorAttachments = &colorAttachment;
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(ctx->encoder, &passDesc);
|
||||
|
||||
if (bindGroup != nullptr) {
|
||||
wgpuRenderPassEncoderSetPipeline(pass, g_debugPresentPipeline);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, bindGroup, 0, nullptr);
|
||||
wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
|
||||
wgpuBindGroupRelease(bindGroup);
|
||||
}
|
||||
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
ModResult open_debug_window() {
|
||||
if (g_debugWindow != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
WindowDesc windowDesc = WINDOW_DESC_INIT;
|
||||
windowDesc.title = "Shadow Debug View";
|
||||
windowDesc.width = 720;
|
||||
windowDesc.height = 480;
|
||||
windowDesc.on_event = on_debug_window_event;
|
||||
auto result = svc_window->create_window(mod_ctx, &windowDesc, &g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
GfxPresentTargetDesc presentDesc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
presentDesc.label = "Shadow debug surface";
|
||||
presentDesc.render = on_debug_present;
|
||||
result = svc_gfx->register_window_present_target(
|
||||
mod_ctx, g_debugWindow, &presentDesc, &g_debugPresentTarget);
|
||||
if (result != MOD_OK) {
|
||||
close_debug_window();
|
||||
return result;
|
||||
}
|
||||
|
||||
result = svc_window->show_window(mod_ctx, g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
close_debug_window();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void on_toggle_debug_window(ModContext*, void*) {
|
||||
const auto result = debug_window_open() ? close_debug_window() : open_debug_window();
|
||||
if (result != MOD_OK) {
|
||||
svc_log->error(mod_ctx, debug_window_open() ? "failed to close shadow debug window" :
|
||||
"failed to open shadow debug window");
|
||||
}
|
||||
}
|
||||
|
||||
// Picks the sun or moon (whichever is above the horizon) and returns the normalized
|
||||
// world-space direction *toward* the light plus a horizon fade factor. False = no light.
|
||||
bool compute_light(float outDirToLight[3], float& outFade) {
|
||||
@@ -596,7 +773,7 @@ void restore_actual_light_debug() {
|
||||
void on_scene_begin(ModContext*, const GfxStageContext* stageCtx, void*) {
|
||||
restore_actual_light_debug();
|
||||
capture_scene_camera(stageCtx);
|
||||
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9) {
|
||||
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9 || debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -654,7 +831,7 @@ void render_shadow_map(
|
||||
return;
|
||||
}
|
||||
const int64_t debugMode = get_debug_mode();
|
||||
if (debugMode == 9) {
|
||||
if (debugMode == 9 && !debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
if (!matrix_ready(replayView)) {
|
||||
@@ -753,16 +930,27 @@ void render_shadow_map(
|
||||
// Game thread, after opaque scene draws and before translucent/fog overlays: deferred composite.
|
||||
void on_scene_after_opaque(ModContext*, const GfxStageContext*, void*) {
|
||||
const int64_t debugMode = get_debug_mode();
|
||||
const bool presentDebug = debug_window_open();
|
||||
restore_actual_light_debug();
|
||||
|
||||
if (presentDebug && debugMode == 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
|
||||
const MapPassOutput mapPass = std::exchange(g_mapPass, {});
|
||||
if (debugMode == 9) {
|
||||
if (debugMode == 9 && !debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
if (!mapPass.ready || mapPass.shadowMap == nullptr || mapPass.lightColor == nullptr) {
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!g_sceneCamera.valid) {
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const CameraInfo& camera = g_sceneCamera.info;
|
||||
@@ -774,6 +962,9 @@ void on_scene_after_opaque(ModContext*, const GfxStageContext*, void*) {
|
||||
if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK ||
|
||||
resolved.depth == nullptr)
|
||||
{
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -807,15 +998,31 @@ void on_scene_after_opaque(ModContext*, const GfxStageContext*, void*) {
|
||||
uniforms.contact_enabled = get_bool_option(g_cvarContactShadows, false) ? 1.0f : 0.0f;
|
||||
uniforms.contact_thickness = 25.0f;
|
||||
uniforms.contact_length = 60.0f;
|
||||
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
|
||||
// Camera Replay intentionally uses the gameplay-camera offscreen pass instead of the light
|
||||
// shadow map, so it remains diagnostic on both windows. Other external diagnostics leave the
|
||||
// main window on the normal shadow composite.
|
||||
uniforms.debug_mode = presentDebug && debugMode != 10 ? 0u : static_cast<uint32_t>(debugMode);
|
||||
|
||||
GfxRange uniformRange{0, 0};
|
||||
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &uniformRange) != MOD_OK) {
|
||||
return;
|
||||
}
|
||||
const DrawPayload payload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
|
||||
uniformRange.offset, uniformRange.size, static_cast<uint32_t>(debugMode)};
|
||||
uniformRange.offset, uniformRange.size, uniforms.debug_mode};
|
||||
svc_gfx->push_draw(mod_ctx, g_drawType, &payload, sizeof(payload));
|
||||
|
||||
if (presentDebug && debugMode != 0) {
|
||||
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
|
||||
GfxRange debugUniformRange{0, 0};
|
||||
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &debugUniformRange) !=
|
||||
MOD_OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const DrawPayload debugPayload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
|
||||
debugUniformRange.offset, debugUniformRange.size, uniforms.debug_mode};
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, &debugPayload, sizeof(debugPayload));
|
||||
}
|
||||
}
|
||||
|
||||
// Frame tail hook: only needed to restore light-view debug camera state before HUD.
|
||||
@@ -905,6 +1112,14 @@ ModResult build_controls_tab(
|
||||
"Bounds: valid X in red, valid Y in green, and valid depth in blue<br/>Light View: "
|
||||
"renders the game world directly from the light camera<br/>Camera Replay: "
|
||||
"captures the same draw-list replay from the gameplay camera");
|
||||
UiControlDesc debugWindowControl = UI_CONTROL_DESC_INIT;
|
||||
debugWindowControl.kind = UI_CONTROL_BUTTON;
|
||||
debugWindowControl.label = "Open / Close Debug Window";
|
||||
debugWindowControl.help_rml =
|
||||
"Shows the selected debug view in an auxiliary WebGPU window. Standard diagnostics leave "
|
||||
"the main view on the normal shadow composite.";
|
||||
debugWindowControl.on_pressed = on_toggle_debug_window;
|
||||
add_control(left, debugWindowControl);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -941,6 +1156,12 @@ ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
control.label = "Open Controls";
|
||||
control.on_pressed = on_open_controls;
|
||||
add_control(panel, control);
|
||||
|
||||
control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_BUTTON;
|
||||
control.label = "Open / Close Debug Window";
|
||||
control.on_pressed = on_toggle_debug_window;
|
||||
add_control(panel, control);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -1063,18 +1284,18 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
// Skip the game's own shadow rendering while the dynamic pass is active: the
|
||||
// shadowControl pair covers the actor real/blob shadows, drawCloudShadow the weather
|
||||
// cloud shadows.
|
||||
if (mods::hook_add_pre<GameShadowImageDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<GameShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<CloudShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK)
|
||||
if (mods::hook::add_pre<GameShadowImageDraw>(on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<GameShadowDraw>(on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<CloudShadowDraw>(on_game_shadow_pre) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering");
|
||||
}
|
||||
if (mods::hook_add_pre<ClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<ClipperBoxClip>(svc_hook, on_frustum_clip_pre) != MOD_OK)
|
||||
if (mods::hook::add_pre<ClipperSphereClip>(on_frustum_clip_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<ClipperBoxClip>(on_frustum_clip_pre) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping");
|
||||
}
|
||||
if (mods::hook_add_pre<CopyTex>(svc_hook, on_copy_tex_pre) != MOD_OK) {
|
||||
if (mods::hook::add_pre<CopyTex>(on_copy_tex_pre) != MOD_OK) {
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex");
|
||||
}
|
||||
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
|
||||
@@ -1090,6 +1311,8 @@ MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
restore_actual_light_debug();
|
||||
close_debug_window();
|
||||
release_debug_present_pipeline();
|
||||
svc_resource->free(mod_ctx, &g_shaderSource);
|
||||
if (g_compositePipeline != nullptr) {
|
||||
wgpuRenderPipelineRelease(g_compositePipeline);
|
||||
@@ -1114,6 +1337,8 @@ MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
g_drawType = g_sceneBeginHook = g_sceneAfterTerrainHook = g_sceneAfterOpaqueHook =
|
||||
g_frameBeforeHudHook = 0;
|
||||
g_controlsWindow = 0;
|
||||
g_debugWindow = 0;
|
||||
g_debugPresentTarget = 0;
|
||||
g_mapPass = {};
|
||||
g_sceneCamera.valid = false;
|
||||
g_sceneCamera.raw_valid = false;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(window_demo CXX)
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
|
||||
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if (DUSK_MOD_USE_FULL_TREE)
|
||||
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
|
||||
else ()
|
||||
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_mod(window_demo
|
||||
FEATURES fmt webgpu
|
||||
SOURCES src/logging.cpp src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
BUNDLE
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.window_demo",
|
||||
"name": "[Demo] Extra Window",
|
||||
"version": "1.0.0",
|
||||
"author": "Twilit Realm",
|
||||
"description": "Demonstrates creating an extra window through WindowService and rendering to it with GfxService."
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "logging.hpp"
|
||||
|
||||
#include "mods/svc/log.hpp"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char* window_event_name(WindowEventType type) {
|
||||
switch (type) {
|
||||
case WINDOW_EVENT_CLOSE_REQUESTED:
|
||||
return "close requested";
|
||||
case WINDOW_EVENT_RESIZED:
|
||||
return "resized";
|
||||
case WINDOW_EVENT_MOVED:
|
||||
return "moved";
|
||||
case WINDOW_EVENT_FOCUS_GAINED:
|
||||
return "focus gained";
|
||||
case WINDOW_EVENT_FOCUS_LOST:
|
||||
return "focus lost";
|
||||
case WINDOW_EVENT_SHOWN:
|
||||
return "shown";
|
||||
case WINDOW_EVENT_HIDDEN:
|
||||
return "hidden";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void window_demo::log_window_event(const WindowEvent* event) {
|
||||
mods::log::info(
|
||||
"window event: {}; position=({}, {}), size={}x{}, pixels={}x{}, scale={:.2f}",
|
||||
window_event_name(event->type), event->x, event->y, event->width, event->height,
|
||||
event->pixel_width, event->pixel_height, event->display_scale);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
struct WindowEvent;
|
||||
|
||||
namespace window_demo {
|
||||
|
||||
void log_window_event(const WindowEvent* event);
|
||||
|
||||
} // namespace window_demo
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "logging.hpp"
|
||||
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/gfx.h"
|
||||
#include "mods/svc/log.hpp"
|
||||
#include "mods/svc/ui.h"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
DEFINE_MOD();
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
IMPORT_SERVICE(UiService, svc_ui);
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
|
||||
namespace {
|
||||
|
||||
WindowHandle g_window = 0;
|
||||
GfxPresentTargetHandle g_presentTarget = 0;
|
||||
GfxStageHookHandle g_stageHook = 0;
|
||||
uint32_t g_frame = 0;
|
||||
bool g_recreatePresentTarget = false;
|
||||
|
||||
struct ClearPayload {
|
||||
float red;
|
||||
float green;
|
||||
float blue;
|
||||
float alpha;
|
||||
};
|
||||
static_assert(sizeof(ClearPayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE);
|
||||
|
||||
ModResult close_window() {
|
||||
g_recreatePresentTarget = false;
|
||||
if (g_presentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_presentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_presentTarget = 0;
|
||||
}
|
||||
if (g_window != 0) {
|
||||
const auto result = svc_window->destroy_window(mod_ctx, g_window);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_window = 0;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void on_window_event(ModContext*, WindowHandle, const WindowEvent* event, void*) {
|
||||
window_demo::log_window_event(event);
|
||||
|
||||
if (event->type == WINDOW_EVENT_CLOSE_REQUESTED) {
|
||||
if (close_window() != MOD_OK) {
|
||||
mods::log::error("failed to close auxiliary window");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render worker thread: record a clear of the acquired auxiliary surface texture.
|
||||
void on_present(
|
||||
ModContext*, const GfxPresentContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
if (payloadSize != sizeof(ClearPayload)) {
|
||||
return;
|
||||
}
|
||||
ClearPayload color;
|
||||
std::memcpy(&color, payload, sizeof(color));
|
||||
|
||||
WGPURenderPassColorAttachment colorAttachment = WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT;
|
||||
colorAttachment.view = ctx->target_view;
|
||||
colorAttachment.loadOp = WGPULoadOp_Clear;
|
||||
colorAttachment.storeOp = WGPUStoreOp_Store;
|
||||
colorAttachment.clearValue = WGPUColor{
|
||||
color.red,
|
||||
color.green,
|
||||
color.blue,
|
||||
color.alpha,
|
||||
};
|
||||
|
||||
WGPURenderPassDescriptor passDesc = WGPU_RENDER_PASS_DESCRIPTOR_INIT;
|
||||
passDesc.label = {"Auxiliary window clear", WGPU_STRLEN};
|
||||
passDesc.colorAttachmentCount = 1;
|
||||
passDesc.colorAttachments = &colorAttachment;
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(ctx->encoder, &passDesc);
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
ModResult register_present_target() {
|
||||
if (g_window == 0 || g_presentTarget != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
GfxPresentTargetDesc presentDesc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
presentDesc.label = "Auxiliary window surface";
|
||||
presentDesc.render = on_present;
|
||||
presentDesc.preferred_alpha_mode =
|
||||
WGPUCompositeAlphaMode_Premultiplied; // For transparent window
|
||||
return svc_gfx->register_window_present_target(
|
||||
mod_ctx, g_window, &presentDesc, &g_presentTarget);
|
||||
}
|
||||
|
||||
ModResult open_window() {
|
||||
if (g_window != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
WindowDesc windowDesc = WINDOW_DESC_INIT;
|
||||
windowDesc.title = "Mod window";
|
||||
windowDesc.width = 640;
|
||||
windowDesc.height = 480;
|
||||
windowDesc.on_event = on_window_event;
|
||||
windowDesc.flags |= WINDOW_FLAG_TRANSPARENT; // For transparent window
|
||||
auto result = svc_window->create_window(mod_ctx, &windowDesc, &g_window);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = register_present_target();
|
||||
if (result != MOD_OK) {
|
||||
close_window();
|
||||
return result;
|
||||
}
|
||||
|
||||
result = svc_window->show_window(mod_ctx, g_window);
|
||||
if (result != MOD_OK) {
|
||||
close_window();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void on_frame_after_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
if (g_presentTarget == 0) {
|
||||
return;
|
||||
}
|
||||
const float phase = static_cast<float>(g_frame++) * 0.015f;
|
||||
const ClearPayload color{
|
||||
.red = 0.08f + 0.06f * (std::sin(phase) + 1.0f),
|
||||
.green = 0.10f + 0.06f * (std::sin(phase + 2.1f) + 1.0f),
|
||||
.blue = 0.14f + 0.08f * (std::sin(phase + 4.2f) + 1.0f),
|
||||
.alpha = 0.5f,
|
||||
};
|
||||
if (svc_gfx->push_present(mod_ctx, g_presentTarget, &color, sizeof(color)) == MOD_ERROR) {
|
||||
g_recreatePresentTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
void on_toggle_window(ModContext*, void*) {
|
||||
if (g_window != 0) {
|
||||
if (close_window() != MOD_OK) {
|
||||
mods::log::error("failed to close auxiliary window");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (open_window() != MOD_OK) {
|
||||
mods::log::error("failed to open auxiliary window");
|
||||
}
|
||||
}
|
||||
|
||||
ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_BUTTON;
|
||||
control.label = "Open / Close Window";
|
||||
control.on_pressed = on_toggle_window;
|
||||
return svc_ui->pane_add_control(mod_ctx, panel, &control, nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT;
|
||||
stageDesc.callback = on_frame_after_hud;
|
||||
if (svc_gfx->register_stage_hook(
|
||||
mod_ctx, GFX_STAGE_FRAME_AFTER_HUD, &stageDesc, &g_stageHook) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register presentation hook");
|
||||
}
|
||||
|
||||
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
|
||||
panelDesc.build = build_panel;
|
||||
if (svc_ui->register_mods_panel(mod_ctx, &panelDesc) != MOD_OK) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register mod panel");
|
||||
}
|
||||
|
||||
if (open_window() != MOD_OK) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
return mods::set_error(error, MOD_ERROR, "failed to open auxiliary window");
|
||||
}
|
||||
|
||||
mods::log::info("auxiliary WebGPU window ready");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError* error) {
|
||||
if (!g_recreatePresentTarget || g_window == 0) {
|
||||
return MOD_OK;
|
||||
}
|
||||
g_recreatePresentTarget = false;
|
||||
if (g_presentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_presentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return mods::set_error(error, result, "failed to unregister lost present target");
|
||||
}
|
||||
g_presentTarget = 0;
|
||||
}
|
||||
const auto result = register_present_target();
|
||||
if (result != MOD_OK) {
|
||||
return mods::set_error(error, result, "failed to recreate present target");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
if (g_stageHook != 0) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
}
|
||||
close_window();
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
#
|
||||
# Usage (from a mod project):
|
||||
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
|
||||
# add_mod(my_mod FEATURES game webgpu SOURCES ... MOD_JSON mod.json)
|
||||
# add_mod(my_mod FEATURES fmt game webgpu SOURCES ... MOD_JSON mod.json)
|
||||
#
|
||||
# On platforms where mods link against the game binary (Windows/Apple/Android), a
|
||||
# version-independent link stub is downloaded automatically unless DUSK_GAME_EXE is set.
|
||||
|
||||
+17
-1
@@ -20,7 +20,23 @@ extern "C" {
|
||||
#ifdef __cplusplus
|
||||
#define MOD_EXTERN_C extern "C"
|
||||
#else
|
||||
#define MOD_EXTERN_C
|
||||
#define MOD_EXTERN_C extern
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define MOD_DECLARE_SERVICE( \
|
||||
service_type, variable, service_id_value, major_value, minor_value) \
|
||||
MOD_EXTERN_C const service_type* variable; \
|
||||
template <> \
|
||||
struct mods::ServiceTraits<service_type> { \
|
||||
static constexpr const char* id = service_id_value; \
|
||||
static constexpr uint16_t major_version = major_value; \
|
||||
static constexpr uint16_t minor_version = minor_value; \
|
||||
}
|
||||
#else
|
||||
#define MOD_DECLARE_SERVICE( \
|
||||
service_type, variable, service_id_value, major_value, minor_value) \
|
||||
MOD_EXTERN_C const service_type* variable
|
||||
#endif
|
||||
|
||||
#define MOD_ABI_VERSION 1u
|
||||
|
||||
+25
-188
@@ -1,219 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_GAME)
|
||||
#error "DEFINE_HOOK requires add_mod(... FEATURES game)"
|
||||
#if defined(_MSC_VER)
|
||||
#pragma message("warning: <mods/hook.hpp> is deprecated; include <mods/svc/hook.hpp> instead")
|
||||
#else
|
||||
#warning "<mods/hook.hpp> is deprecated; include <mods/svc/hook.hpp> instead"
|
||||
#endif
|
||||
|
||||
#include <mods/svc/hook.h>
|
||||
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <mods/svc/hook.hpp>
|
||||
|
||||
namespace mods {
|
||||
|
||||
template <class T>
|
||||
T arg(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Trampoline generator + per-target state. Tag makes each hooked target's statics distinct; the
|
||||
* target address comes from the declaration's metadata record, resolved by the host at mod
|
||||
* initialization.
|
||||
*/
|
||||
template <class Tag, class R, class... A>
|
||||
struct HookImpl {
|
||||
static inline R (*g_orig)(A...) = nullptr;
|
||||
static inline const HookService* hooks = nullptr;
|
||||
static inline void* target = nullptr;
|
||||
|
||||
static bool dispatch_pre(void* args, void* retval) {
|
||||
if (hooks == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int skipOriginal = 0;
|
||||
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
|
||||
return result == MOD_OK && skipOriginal != 0;
|
||||
}
|
||||
|
||||
static void dispatch_post(void* args, void* retval) {
|
||||
if (hooks != nullptr) {
|
||||
hooks->dispatch_post(mod_ctx, target, args, retval);
|
||||
}
|
||||
}
|
||||
|
||||
static R trampoline(A... args) {
|
||||
if constexpr (sizeof...(A) == 0) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal =
|
||||
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal = dispatch_pre(
|
||||
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <auto Target>
|
||||
using TargetTag = std::integral_constant<decltype(Target), Target>;
|
||||
template <FixedString Name>
|
||||
struct NameTag {};
|
||||
} // namespace detail
|
||||
|
||||
/*
|
||||
* Typed base for a hook on a function named at compile time (&daAlink_c::execute, &free_fn).
|
||||
* Instantiate through DEFINE_HOOK, which pairs it with the metadata record the host resolves.
|
||||
*/
|
||||
template <auto Target>
|
||||
struct Hook;
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {};
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...) const>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {};
|
||||
|
||||
template <class R, class... A, R (*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {};
|
||||
|
||||
/*
|
||||
* Typed base for a hook on a function by its symbol name, for targets you can't name in C++:
|
||||
* file-local statics, private members, or symbols without a header. The signature is written
|
||||
* free-style with the receiver first and is *not* compiler-checked. Instantiate through
|
||||
* DEFINE_HOOK_SYMBOL.
|
||||
*/
|
||||
template <FixedString Name, class Sig>
|
||||
struct NamedHook;
|
||||
|
||||
template <FixedString Name, class R, class... A>
|
||||
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, R, A...> {};
|
||||
|
||||
/*
|
||||
* Declare a hook target. The declaration emits a metadata record that the host resolves at mod
|
||||
* initialization. Every hook target must be declared.
|
||||
*
|
||||
* DEFINE_HOOK(&daAlink_c::execute, LinkExecute);
|
||||
* DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
|
||||
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
|
||||
*
|
||||
* mods::hook_add_pre<LinkExecute>(svc_hook, on_link_execute);
|
||||
*
|
||||
* DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O
|
||||
* leading underscore) or the demangled qualified display name; overloaded display names are
|
||||
* ambiguous and need the mangled form.
|
||||
*/
|
||||
#if defined(__GNUC__) && !defined(__clang__) && defined(__ELF__)
|
||||
#define DEFINE_HOOK(target, alias) \
|
||||
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
|
||||
::mods::detail::make_local_hook_record<(target), ::mods::FixedString{#target}>(); \
|
||||
struct alias : ::mods::Hook<(target)> { \
|
||||
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
|
||||
}
|
||||
#else
|
||||
#define DEFINE_HOOK(target, alias) \
|
||||
[[maybe_unused]] static const void* const mod_meta_hook_##alias = \
|
||||
&::mods::detail::HookRecordFor<(target), ::mods::FixedString{#target}>::Holder::record; \
|
||||
struct alias : ::mods::Hook<(target)> { \
|
||||
static void* resolved_target() { \
|
||||
return ::mods::detail::HookRecordFor<(target), \
|
||||
::mods::FixedString{#target}>::Holder::record.resolved; \
|
||||
} \
|
||||
}
|
||||
#endif
|
||||
|
||||
#define DEFINE_HOOK_SYMBOL(name, sig, alias) \
|
||||
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
|
||||
::mods::detail::make_hook_name_record<::mods::FixedString{name}>(); \
|
||||
struct alias : ::mods::NamedHook<::mods::FixedString{name}, sig> { \
|
||||
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
if (hooks == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return hook::install<Entry>(hooks);
|
||||
}
|
||||
|
||||
Entry::hooks = hooks;
|
||||
if (Entry::target == nullptr) {
|
||||
void* resolved = Entry::resolved_target();
|
||||
if (resolved == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
Entry::target = resolved;
|
||||
}
|
||||
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
|
||||
reinterpret_cast<void**>(&Entry::g_orig));
|
||||
template <class Entry>
|
||||
ModResult hook_install() {
|
||||
return hook::install<Entry>();
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::add_pre<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_add_pre(HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::add_pre<Entry>(callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::add_post<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->add_post(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_add_post(HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::add_post<Entry>(callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::replace<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->replace(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_replace(HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::replace<Entry>(callback, options);
|
||||
}
|
||||
|
||||
} // namespace mods
|
||||
|
||||
@@ -40,13 +40,13 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa
|
||||
}; \
|
||||
}
|
||||
|
||||
// Declares `static const service_type* variable`, filled in by the host before mod_initialize.
|
||||
// Defines `const service_type* variable`, filled in by the host before mod_initialize.
|
||||
// Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports
|
||||
// must be checked against nullptr before use. The unversioned macros use the latest minor version;
|
||||
// set an explicit version to target an older minor version for backwards compatibility.
|
||||
#define IMPORT_SERVICE_EX( \
|
||||
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
|
||||
static const service_type* variable = nullptr; \
|
||||
const service_type* variable = nullptr; \
|
||||
MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = { \
|
||||
{sizeof(ModMetaImport), MOD_META_IMPORT, static_cast<uint8_t>(flags_value)}, \
|
||||
static_cast<uint16_t>(major_value), \
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera"
|
||||
#define CAMERA_SERVICE_MAJOR 1u
|
||||
#define CAMERA_SERVICE_MINOR 0u
|
||||
@@ -53,13 +57,5 @@ typedef struct CameraService {
|
||||
ModResult (*get_camera)(ModContext* ctx, const void* game_view, CameraInfo* out_info);
|
||||
} CameraService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<CameraService> {
|
||||
static constexpr const char* id = CAMERA_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CAMERA_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = CAMERA_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(
|
||||
CameraService, svc_camera, CAMERA_SERVICE_ID, CAMERA_SERVICE_MAJOR, CAMERA_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config"
|
||||
#define CONFIG_SERVICE_MAJOR 1u
|
||||
#define CONFIG_SERVICE_MINOR 0u
|
||||
@@ -96,13 +100,5 @@ typedef struct ConfigService {
|
||||
ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle);
|
||||
} ConfigService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<ConfigService> {
|
||||
static constexpr const char* id = CONFIG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = CONFIG_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(
|
||||
ConfigService, svc_config, CONFIG_SERVICE_ID, CONFIG_SERVICE_MAJOR, CONFIG_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* The mod SDK imports this service automatically for mods built with FEATURES game; service-only
|
||||
* and asset-only mods do not require it.
|
||||
@@ -19,13 +23,4 @@ typedef struct GameService {
|
||||
ServiceHeader header;
|
||||
} GameService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<GameService> {
|
||||
static constexpr const char* id = GAME_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = GAME_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(GameService, svc_game, GAME_SERVICE_ID, GAME_SERVICE_MAJOR, GAME_SERVICE_MINOR);
|
||||
|
||||
+72
-12
@@ -1,6 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
#include <mods/svc/window.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_WEBGPU)
|
||||
#error "mods/svc/gfx.h requires add_mod(... FEATURES webgpu)"
|
||||
@@ -28,7 +33,7 @@
|
||||
|
||||
#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx"
|
||||
#define GFX_SERVICE_MAJOR 1u
|
||||
#define GFX_SERVICE_MINOR 0u
|
||||
#define GFX_SERVICE_MINOR 1u
|
||||
|
||||
/* Maximum size for push_draw payload */
|
||||
#define GFX_INLINE_DRAW_PAYLOAD_SIZE 128u
|
||||
@@ -37,6 +42,7 @@
|
||||
typedef uint64_t GfxDrawTypeHandle;
|
||||
typedef uint64_t GfxStageHookHandle;
|
||||
typedef uint64_t GfxComputeTypeHandle;
|
||||
typedef uint64_t GfxPresentTargetHandle;
|
||||
|
||||
/* A suballocation in one of the shared per-frame streaming buffers. */
|
||||
typedef struct GfxRange {
|
||||
@@ -56,11 +62,13 @@ typedef struct GfxDeviceInfo {
|
||||
WGPUTextureFormat depth_format; /* scene depth target format */
|
||||
uint32_t sample_count; /* scene pass MSAA sample count */
|
||||
bool uses_reversed_z; /* true means depth 1.0 is near */
|
||||
WGPUInstance instance; /* borrowed; added in GfxService 1.1 */
|
||||
WGPUAdapter adapter; /* borrowed; added in GfxService 1.1 */
|
||||
} GfxDeviceInfo;
|
||||
|
||||
#define GFX_DEVICE_INFO_INIT \
|
||||
{sizeof(GfxDeviceInfo), NULL, NULL, WGPUTextureFormat_Undefined, WGPUTextureFormat_Undefined, \
|
||||
1u, false}
|
||||
1u, false, NULL, NULL}
|
||||
|
||||
/*
|
||||
* Passed to GfxDrawFn on the render worker thread; valid only during the call. The pass pipeline,
|
||||
@@ -168,6 +176,48 @@ typedef struct GfxComputeTypeDesc {
|
||||
|
||||
#define GFX_COMPUTE_TYPE_DESC_INIT {sizeof(GfxComputeTypeDesc), NULL, NULL, NULL}
|
||||
|
||||
/*
|
||||
* Invoked on the render worker while the frame encoder is open. The target texture and view have
|
||||
* been acquired by the host and are borrowed for the callback. Record all target work on encoder,
|
||||
* leave no pass open, and do not finish, submit, or present it. The host submits the shared command
|
||||
* buffer and presents the target after submission. The streaming buffers contain data appended on
|
||||
* the game thread before push_present.
|
||||
*/
|
||||
typedef struct GfxPresentContext {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device;
|
||||
WGPUQueue queue;
|
||||
WGPUCommandEncoder encoder;
|
||||
WGPUTexture target_texture;
|
||||
WGPUTextureView target_view;
|
||||
WGPUTextureFormat target_format;
|
||||
uint32_t target_width;
|
||||
uint32_t target_height;
|
||||
WGPUBuffer vertex_buffer;
|
||||
WGPUBuffer index_buffer;
|
||||
WGPUBuffer uniform_buffer;
|
||||
WGPUBuffer storage_buffer;
|
||||
} GfxPresentContext;
|
||||
|
||||
typedef void (*GfxPresentFn)(ModContext* ctx, const GfxPresentContext* present_ctx,
|
||||
const void* payload, size_t payload_size, void* user_data);
|
||||
|
||||
typedef struct GfxPresentTargetDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* optional debug label */
|
||||
uint32_t width; /* required for raw surfaces; ignored for WindowService windows */
|
||||
uint32_t height;
|
||||
WGPUTextureUsage usage; /* 0 defaults to RenderAttachment */
|
||||
WGPUTextureFormat preferred_format;
|
||||
WGPUCompositeAlphaMode preferred_alpha_mode;
|
||||
GfxPresentFn render;
|
||||
void* user_data;
|
||||
} GfxPresentTargetDesc;
|
||||
|
||||
#define GFX_PRESENT_TARGET_DESC_INIT \
|
||||
{sizeof(GfxPresentTargetDesc), NULL, 0u, 0u, WGPUTextureUsage_None, \
|
||||
WGPUTextureFormat_Undefined, WGPUCompositeAlphaMode_Auto, NULL, NULL}
|
||||
|
||||
typedef struct GfxService {
|
||||
ServiceHeader header;
|
||||
|
||||
@@ -200,15 +250,25 @@ typedef struct GfxService {
|
||||
ModResult (*resolve_pass)(
|
||||
ModContext* ctx, const GfxResolveDesc* desc, GfxResolvedTargets* out_targets);
|
||||
ModResult (*create_pass)(ModContext* ctx, uint32_t width, uint32_t height);
|
||||
|
||||
/* Minor version 1 */
|
||||
|
||||
ModResult (*register_present_target)(ModContext* ctx, WGPUSurface surface,
|
||||
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* out_handle);
|
||||
ModResult (*register_window_present_target)(ModContext* ctx, WindowHandle window,
|
||||
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* out_handle);
|
||||
/* Raw-surface targets only; WindowService target resizes are managed automatically. */
|
||||
ModResult (*resize_present_target)(
|
||||
ModContext* ctx, GfxPresentTargetHandle handle, uint32_t width, uint32_t height);
|
||||
ModResult (*unregister_present_target)(ModContext* ctx, GfxPresentTargetHandle handle);
|
||||
/*
|
||||
* MOD_OK means the task was queued.
|
||||
* MOD_UNAVAILABLE means no task could be queued now (for example, a window has no pixel size).
|
||||
* MOD_ERROR means an earlier task found the surface lost or deterministically invalid;
|
||||
* unregister and recreate the target before pushing again.
|
||||
*/
|
||||
ModResult (*push_present)(
|
||||
ModContext* ctx, GfxPresentTargetHandle handle, const void* payload, size_t payload_size);
|
||||
} GfxService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<GfxService> {
|
||||
static constexpr const char* id = GFX_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GFX_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = GFX_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(GfxService, svc_gfx, GFX_SERVICE_ID, GFX_SERVICE_MAJOR, GFX_SERVICE_MINOR);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user