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

This commit is contained in:
Luke Street
2026-07-30 00:24:24 -06:00
committed by GitHub
parent 48de4bcb08
commit 7305ef09b9
36 changed files with 2195 additions and 395 deletions
+257 -32
View File
@@ -20,7 +20,7 @@
#include "dolphin/gx/GXPixel.h"
#include "dolphin/gx/GXTransform.h"
#include "m_Do/m_Do_mtx.h"
#include "mods/hook.hpp"
#include "mods/svc/hook.hpp"
#include "mods/service.hpp"
#include "mods/svc/camera.h"
#include "mods/svc/config.h"
@@ -29,6 +29,7 @@
#include "mods/svc/log.h"
#include "mods/svc/resource.h"
#include "mods/svc/ui.h"
#include "mods/svc/window.h"
#include <algorithm>
#include <cmath>
@@ -45,6 +46,7 @@ IMPORT_SERVICE(GfxService, svc_gfx);
IMPORT_SERVICE(CameraService, svc_camera);
IMPORT_SERVICE(HookService, svc_hook);
IMPORT_SERVICE(LogService, svc_log);
IMPORT_SERVICE(WindowService, svc_window);
namespace {
@@ -71,6 +73,11 @@ WGPURenderPipeline g_compositePipeline = nullptr; // multiply blend
WGPURenderPipeline g_compositeDebugPipeline = nullptr; // no blend (debug views)
WGPUBindGroupLayout g_compositeLayout = nullptr;
WGPUBindGroupLayout g_compositeDebugLayout = nullptr;
WGPURenderPipeline g_debugPresentPipeline = nullptr;
WGPUBindGroupLayout g_debugPresentLayout = nullptr;
WGPUTextureFormat g_debugPresentFormat = WGPUTextureFormat_Undefined;
WindowHandle g_debugWindow = 0;
GfxPresentTargetHandle g_debugPresentTarget = 0;
struct MapPassOutput {
bool ready = false;
@@ -188,6 +195,34 @@ int64_t get_debug_mode() {
return std::clamp<int64_t>(get_int_option(g_cvarDebugView, 0), 0, 10);
}
bool debug_window_open() {
return g_debugWindow != 0 && g_debugPresentTarget != 0;
}
ModResult close_debug_window() {
if (g_debugPresentTarget != 0) {
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_debugPresentTarget);
if (result != MOD_OK) {
return result;
}
g_debugPresentTarget = 0;
}
if (g_debugWindow != 0) {
const auto result = svc_window->destroy_window(mod_ctx, g_debugWindow);
if (result != MOD_OK) {
return result;
}
g_debugWindow = 0;
}
return MOD_OK;
}
void on_debug_window_event(ModContext*, WindowHandle, const WindowEvent* event, void*) {
if (event->type == WINDOW_EVENT_CLOSE_REQUESTED && close_debug_window() != MOD_OK) {
svc_log->error(mod_ctx, "failed to close shadow debug window");
}
}
bool matrix_ready(const Mtx m) {
float basis = 0.0f;
for (int r = 0; r < 3; ++r) {
@@ -396,6 +431,90 @@ bool build_composite_pipeline(
return outLayout != nullptr;
}
void release_debug_present_pipeline() {
if (g_debugPresentPipeline != nullptr) {
wgpuRenderPipelineRelease(g_debugPresentPipeline);
g_debugPresentPipeline = nullptr;
}
if (g_debugPresentLayout != nullptr) {
wgpuBindGroupLayoutRelease(g_debugPresentLayout);
g_debugPresentLayout = nullptr;
}
g_debugPresentFormat = WGPUTextureFormat_Undefined;
}
bool ensure_debug_present_pipeline(const GfxPresentContext& ctx) {
if (g_debugPresentPipeline != nullptr && g_debugPresentFormat == ctx.target_format) {
return true;
}
release_debug_present_pipeline();
WGPUShaderSourceWGSL wgsl = WGPU_SHADER_SOURCE_WGSL_INIT;
wgsl.code = {static_cast<const char*>(g_shaderSource.data), g_shaderSource.size};
WGPUShaderModuleDescriptor moduleDesc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT;
moduleDesc.nextInChain = &wgsl.chain;
moduleDesc.label = {"shadow debug present", WGPU_STRLEN};
WGPUShaderModule module = wgpuDeviceCreateShaderModule(ctx.device, &moduleDesc);
if (module == nullptr) {
return false;
}
WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT;
colorTarget.format = ctx.target_format;
WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT;
fragment.module = module;
fragment.entryPoint = {"fs_main", WGPU_STRLEN};
fragment.targetCount = 1;
fragment.targets = &colorTarget;
WGPURenderPipelineDescriptor pipelineDesc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT;
pipelineDesc.label = {"shadow debug present", WGPU_STRLEN};
pipelineDesc.vertex.module = module;
pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN};
pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
pipelineDesc.multisample.count = 1;
pipelineDesc.fragment = &fragment;
g_debugPresentPipeline = wgpuDeviceCreateRenderPipeline(ctx.device, &pipelineDesc);
wgpuShaderModuleRelease(module);
if (g_debugPresentPipeline == nullptr) {
return false;
}
g_debugPresentLayout = wgpuRenderPipelineGetBindGroupLayout(g_debugPresentPipeline, 0);
if (g_debugPresentLayout == nullptr) {
release_debug_present_pipeline();
return false;
}
g_debugPresentFormat = ctx.target_format;
return true;
}
WGPUBindGroup create_composite_bind_group(WGPUDevice device, WGPUBindGroupLayout layout,
WGPUBuffer uniformBuffer, const DrawPayload& data) {
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
layout == nullptr || uniformBuffer == nullptr)
{
return nullptr;
}
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
entries[0].binding = 0;
entries[0].textureView = data.sceneDepth;
entries[1].binding = 1;
entries[1].textureView = data.shadowMap;
entries[2].binding = 2;
entries[2].buffer = uniformBuffer;
entries[2].offset = data.uniform_offset;
entries[2].size = data.uniform_size;
entries[3].binding = 3;
entries[3].textureView = data.lightColor;
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
bindGroupDesc.layout = layout;
bindGroupDesc.entryCount = 4;
bindGroupDesc.entries = entries;
return wgpuDeviceCreateBindGroup(device, &bindGroupDesc);
}
// Render worker thread: fullscreen deferred-shadow composite.
void on_draw(
ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) {
@@ -408,29 +527,12 @@ void on_draw(
WGPURenderPipeline pipeline =
data.debug_mode != 0 ? g_compositeDebugPipeline : g_compositePipeline;
WGPUBindGroupLayout layout = data.debug_mode != 0 ? g_compositeDebugLayout : g_compositeLayout;
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
pipeline == nullptr)
{
if (pipeline == nullptr) {
return;
}
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
entries[0].binding = 0;
entries[0].textureView = data.sceneDepth;
entries[1].binding = 1;
entries[1].textureView = data.shadowMap;
entries[2].binding = 2;
entries[2].buffer = ctx->uniform_buffer;
entries[2].offset = data.uniform_offset;
entries[2].size = data.uniform_size;
entries[3].binding = 3;
entries[3].textureView = data.lightColor;
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
bindGroupDesc.layout = layout;
bindGroupDesc.entryCount = 4;
bindGroupDesc.entries = entries;
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
WGPUBindGroup bindGroup =
create_composite_bind_group(ctx->device, layout, ctx->uniform_buffer, data);
if (bindGroup == nullptr) {
return;
}
@@ -441,6 +543,81 @@ void on_draw(
wgpuBindGroupRelease(bindGroup);
}
// Render worker thread: draw the selected diagnostic into the auxiliary surface.
void on_debug_present(
ModContext*, const GfxPresentContext* ctx, const void* payload, size_t payloadSize, void*) {
WGPUBindGroup bindGroup = nullptr;
if (payloadSize == sizeof(DrawPayload)) {
DrawPayload data;
std::memcpy(&data, payload, sizeof(data));
if (ensure_debug_present_pipeline(*ctx)) {
bindGroup = create_composite_bind_group(
ctx->device, g_debugPresentLayout, ctx->uniform_buffer, data);
}
}
WGPURenderPassColorAttachment colorAttachment = WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT;
colorAttachment.view = ctx->target_view;
colorAttachment.loadOp = WGPULoadOp_Clear;
colorAttachment.storeOp = WGPUStoreOp_Store;
colorAttachment.clearValue = WGPUColor{0.0, 0.0, 0.0, 1.0};
WGPURenderPassDescriptor passDesc = WGPU_RENDER_PASS_DESCRIPTOR_INIT;
passDesc.label = {"shadow debug present", WGPU_STRLEN};
passDesc.colorAttachmentCount = 1;
passDesc.colorAttachments = &colorAttachment;
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(ctx->encoder, &passDesc);
if (bindGroup != nullptr) {
wgpuRenderPassEncoderSetPipeline(pass, g_debugPresentPipeline);
wgpuRenderPassEncoderSetBindGroup(pass, 0, bindGroup, 0, nullptr);
wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
wgpuBindGroupRelease(bindGroup);
}
wgpuRenderPassEncoderEnd(pass);
wgpuRenderPassEncoderRelease(pass);
}
ModResult open_debug_window() {
if (g_debugWindow != 0) {
return MOD_CONFLICT;
}
WindowDesc windowDesc = WINDOW_DESC_INIT;
windowDesc.title = "Shadow Debug View";
windowDesc.width = 720;
windowDesc.height = 480;
windowDesc.on_event = on_debug_window_event;
auto result = svc_window->create_window(mod_ctx, &windowDesc, &g_debugWindow);
if (result != MOD_OK) {
return result;
}
GfxPresentTargetDesc presentDesc = GFX_PRESENT_TARGET_DESC_INIT;
presentDesc.label = "Shadow debug surface";
presentDesc.render = on_debug_present;
result = svc_gfx->register_window_present_target(
mod_ctx, g_debugWindow, &presentDesc, &g_debugPresentTarget);
if (result != MOD_OK) {
close_debug_window();
return result;
}
result = svc_window->show_window(mod_ctx, g_debugWindow);
if (result != MOD_OK) {
close_debug_window();
}
return result;
}
void on_toggle_debug_window(ModContext*, void*) {
const auto result = debug_window_open() ? close_debug_window() : open_debug_window();
if (result != MOD_OK) {
svc_log->error(mod_ctx, debug_window_open() ? "failed to close shadow debug window" :
"failed to open shadow debug window");
}
}
// Picks the sun or moon (whichever is above the horizon) and returns the normalized
// world-space direction *toward* the light plus a horizon fade factor. False = no light.
bool compute_light(float outDirToLight[3], float& outFade) {
@@ -596,7 +773,7 @@ void restore_actual_light_debug() {
void on_scene_begin(ModContext*, const GfxStageContext* stageCtx, void*) {
restore_actual_light_debug();
capture_scene_camera(stageCtx);
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9) {
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9 || debug_window_open()) {
return;
}
@@ -654,7 +831,7 @@ void render_shadow_map(
return;
}
const int64_t debugMode = get_debug_mode();
if (debugMode == 9) {
if (debugMode == 9 && !debug_window_open()) {
return;
}
if (!matrix_ready(replayView)) {
@@ -753,16 +930,27 @@ void render_shadow_map(
// Game thread, after opaque scene draws and before translucent/fog overlays: deferred composite.
void on_scene_after_opaque(ModContext*, const GfxStageContext*, void*) {
const int64_t debugMode = get_debug_mode();
const bool presentDebug = debug_window_open();
restore_actual_light_debug();
if (presentDebug && debugMode == 0) {
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
}
const MapPassOutput mapPass = std::exchange(g_mapPass, {});
if (debugMode == 9) {
if (debugMode == 9 && !debug_window_open()) {
return;
}
if (!mapPass.ready || mapPass.shadowMap == nullptr || mapPass.lightColor == nullptr) {
if (presentDebug && debugMode != 0) {
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
}
return;
}
if (!g_sceneCamera.valid) {
if (presentDebug && debugMode != 0) {
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
}
return;
}
const CameraInfo& camera = g_sceneCamera.info;
@@ -774,6 +962,9 @@ void on_scene_after_opaque(ModContext*, const GfxStageContext*, void*) {
if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK ||
resolved.depth == nullptr)
{
if (presentDebug && debugMode != 0) {
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
}
return;
}
@@ -807,15 +998,31 @@ void on_scene_after_opaque(ModContext*, const GfxStageContext*, void*) {
uniforms.contact_enabled = get_bool_option(g_cvarContactShadows, false) ? 1.0f : 0.0f;
uniforms.contact_thickness = 25.0f;
uniforms.contact_length = 60.0f;
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
// Camera Replay intentionally uses the gameplay-camera offscreen pass instead of the light
// shadow map, so it remains diagnostic on both windows. Other external diagnostics leave the
// main window on the normal shadow composite.
uniforms.debug_mode = presentDebug && debugMode != 10 ? 0u : static_cast<uint32_t>(debugMode);
GfxRange uniformRange{0, 0};
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &uniformRange) != MOD_OK) {
return;
}
const DrawPayload payload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
uniformRange.offset, uniformRange.size, static_cast<uint32_t>(debugMode)};
uniformRange.offset, uniformRange.size, uniforms.debug_mode};
svc_gfx->push_draw(mod_ctx, g_drawType, &payload, sizeof(payload));
if (presentDebug && debugMode != 0) {
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
GfxRange debugUniformRange{0, 0};
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &debugUniformRange) !=
MOD_OK)
{
return;
}
const DrawPayload debugPayload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
debugUniformRange.offset, debugUniformRange.size, uniforms.debug_mode};
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, &debugPayload, sizeof(debugPayload));
}
}
// Frame tail hook: only needed to restore light-view debug camera state before HUD.
@@ -905,6 +1112,14 @@ ModResult build_controls_tab(
"Bounds: valid X in red, valid Y in green, and valid depth in blue<br/>Light View: "
"renders the game world directly from the light camera<br/>Camera Replay: "
"captures the same draw-list replay from the gameplay camera");
UiControlDesc debugWindowControl = UI_CONTROL_DESC_INIT;
debugWindowControl.kind = UI_CONTROL_BUTTON;
debugWindowControl.label = "Open / Close Debug Window";
debugWindowControl.help_rml =
"Shows the selected debug view in an auxiliary WebGPU window. Standard diagnostics leave "
"the main view on the normal shadow composite.";
debugWindowControl.on_pressed = on_toggle_debug_window;
add_control(left, debugWindowControl);
return MOD_OK;
}
@@ -941,6 +1156,12 @@ ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
control.label = "Open Controls";
control.on_pressed = on_open_controls;
add_control(panel, control);
control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_BUTTON;
control.label = "Open / Close Debug Window";
control.on_pressed = on_toggle_debug_window;
add_control(panel, control);
return MOD_OK;
}
@@ -1063,18 +1284,18 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
// Skip the game's own shadow rendering while the dynamic pass is active: the
// shadowControl pair covers the actor real/blob shadows, drawCloudShadow the weather
// cloud shadows.
if (mods::hook_add_pre<GameShadowImageDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
mods::hook_add_pre<GameShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
mods::hook_add_pre<CloudShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK)
if (mods::hook::add_pre<GameShadowImageDraw>(on_game_shadow_pre) != MOD_OK ||
mods::hook::add_pre<GameShadowDraw>(on_game_shadow_pre) != MOD_OK ||
mods::hook::add_pre<CloudShadowDraw>(on_game_shadow_pre) != MOD_OK)
{
return mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering");
}
if (mods::hook_add_pre<ClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
mods::hook_add_pre<ClipperBoxClip>(svc_hook, on_frustum_clip_pre) != MOD_OK)
if (mods::hook::add_pre<ClipperSphereClip>(on_frustum_clip_pre) != MOD_OK ||
mods::hook::add_pre<ClipperBoxClip>(on_frustum_clip_pre) != MOD_OK)
{
return mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping");
}
if (mods::hook_add_pre<CopyTex>(svc_hook, on_copy_tex_pre) != MOD_OK) {
if (mods::hook::add_pre<CopyTex>(on_copy_tex_pre) != MOD_OK) {
return mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex");
}
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
@@ -1090,6 +1311,8 @@ MOD_EXPORT ModResult mod_update(ModError*) {
MOD_EXPORT ModResult mod_shutdown(ModError*) {
restore_actual_light_debug();
close_debug_window();
release_debug_present_pipeline();
svc_resource->free(mod_ctx, &g_shaderSource);
if (g_compositePipeline != nullptr) {
wgpuRenderPipelineRelease(g_compositePipeline);
@@ -1114,6 +1337,8 @@ MOD_EXPORT ModResult mod_shutdown(ModError*) {
g_drawType = g_sceneBeginHook = g_sceneAfterTerrainHook = g_sceneAfterOpaqueHook =
g_frameBeforeHudHook = 0;
g_controlsWindow = 0;
g_debugWindow = 0;
g_debugPresentTarget = 0;
g_mapPass = {};
g_sceneCamera.valid = false;
g_sceneCamera.raw_valid = false;