// Ambient occlusion (GTAO) example mod. // // Showcases the gfx service's compute tasks and the camera service: after opaque scene draws, // before translucent/fog overlays, the scene depth is resolved and a three-dispatch compute // chain (depth MIP prefilter, GTAO, spatial denoise) produces a visibility texture that a // fullscreen draw multiplies over the world. // // The WGSL in res/ is ported from Bevy Engine's SSAO implementation (MIT OR Apache-2.0), // itself based on Intel XeGTAO (MIT); see res/licenses/ and the `PORT:` notes in the shaders. #include "mods/service.hpp" #include "mods/svc/camera.h" #include "mods/svc/config.h" #include "mods/svc/gfx.h" #include "mods/svc/log.h" #include "mods/svc/resource.h" #include "mods/svc/ui.h" #include #include #include #include #include #include #include #include DEFINE_MOD(); IMPORT_SERVICE(LogService, svc_log); IMPORT_SERVICE(ConfigService, svc_config); IMPORT_SERVICE(ResourceService, svc_resource); IMPORT_SERVICE(UiService, svc_ui); IMPORT_SERVICE(GfxService, svc_gfx); IMPORT_SERVICE(CameraService, svc_camera); namespace { ConfigVarHandle g_cvarEnabled = 0; ConfigVarHandle g_cvarQuality = 0; ConfigVarHandle g_cvarRadius = 0; ConfigVarHandle g_cvarIntensity = 0; ConfigVarHandle g_cvarHalfRes = 0; ConfigVarHandle g_cvarDebugView = 0; GfxComputeTypeHandle g_computeType = 0; GfxDrawTypeHandle g_drawType = 0; GfxStageHookHandle g_afterOpaqueHook = 0; UiWindowHandle g_controlsWindow = 0; ResourceBuffer g_preprocessSource = RESOURCE_BUFFER_INIT; ResourceBuffer g_gtaoSource = RESOURCE_BUFFER_INIT; ResourceBuffer g_denoiseSource = RESOURCE_BUFFER_INIT; ResourceBuffer g_compositeSource = RESOURCE_BUFFER_INIT; GfxDeviceInfo g_deviceInfo = GFX_DEVICE_INFO_INIT; WGPUComputePipeline g_preprocessPipeline = nullptr; WGPUComputePipeline g_mip4Pipeline = nullptr; WGPUComputePipeline g_gtaoPipeline = nullptr; WGPUComputePipeline g_denoisePipeline = nullptr; WGPUBindGroupLayout g_preprocessLayout = nullptr; WGPUBindGroupLayout g_mip4Layout = nullptr; WGPUBindGroupLayout g_gtaoLayout = nullptr; WGPUBindGroupLayout g_denoiseLayout = nullptr; WGPURenderPipeline g_compositePipeline = nullptr; WGPURenderPipeline g_compositeDebugPipeline = nullptr; WGPUBindGroupLayout g_compositeLayout = nullptr; WGPUBindGroupLayout g_compositeDebugLayout = nullptr; WGPUTexture g_hilbertLut = nullptr; WGPUTextureView g_hilbertLutView = nullptr; // AO chain targets, recreated when the render size (or halfRes) changes. Old sets are retired // for a few frames instead of released immediately: payloads embedding their views may still // be in flight on the render worker. struct AoTargets { uint32_t width = 0; uint32_t height = 0; WGPUTexture preprocessedDepth = nullptr; WGPUTextureView preprocessedDepthMips[5] = {}; WGPUTextureView preprocessedDepthAll = nullptr; WGPUTexture aoNoisy = nullptr; WGPUTextureView aoNoisyView = nullptr; WGPUTexture depthDifferences = nullptr; WGPUTextureView depthDifferencesView = nullptr; WGPUTexture aoFinal = nullptr; WGPUTextureView aoFinalView = nullptr; }; AoTargets g_targets; struct RetiredTargets { AoTargets targets; int framesLeft = 0; }; std::vector g_retiredTargets; bool g_warnedNoDepth = false; bool g_loggedChain = false; std::atomic g_chainExecuted{false}; // Mirror of the WGSL Uniforms struct (keep in sync with res/*.wgsl). struct AoUniforms { float projection[16]; float inverse_projection[16]; float size[2]; float inv_size[2]; float depth_scale[2]; float effect_radius; float intensity; float slice_count; float samples_per_slice_side; uint32_t debug_view; float _pad; }; static_assert(sizeof(AoUniforms) % 16 == 0); struct ComputePayload { WGPUTextureView depth; // frame-pooled scene depth snapshot WGPUTextureView preprocessedDepthMips[5]; WGPUTextureView preprocessedDepthAll; WGPUTextureView aoNoisy; WGPUTextureView depthDifferences; WGPUTextureView aoFinal; uint32_t uniform_offset; uint32_t uniform_size; uint32_t width; uint32_t height; }; static_assert(sizeof(ComputePayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE); static_assert(std::is_trivially_copyable_v); struct CompositePayload { WGPUTextureView aoFinal; WGPUTextureView preprocessedDepth; // debug views reconstruct normals/depth from it WGPUTextureView sceneDepth; // raw snapshot, for the bypass debug views uint32_t uniform_offset; uint32_t uniform_size; uint32_t debug_view; }; static_assert(sizeof(CompositePayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE); static_assert(std::is_trivially_copyable_v); int64_t get_int_option(ConfigVarHandle handle, int64_t fallback) { int64_t value = fallback; if (handle == 0 || svc_config->get_int(mod_ctx, handle, &value) != MOD_OK) { return fallback; } return value; } bool get_bool_option(ConfigVarHandle handle, bool fallback) { bool value = fallback; if (handle == 0 || svc_config->get_bool(mod_ctx, handle, &value) != MOD_OK) { return fallback; } return value; } // XeGTAO/Bevy quality presets: slices x (samples per slice side * 2). void quality_counts(int64_t quality, float& sliceCount, float& samplesPerSliceSide) { switch (std::clamp(quality, 0, 3)) { case 0: sliceCount = 1.0f; samplesPerSliceSide = 2.0f; break; case 1: sliceCount = 2.0f; samplesPerSliceSide = 2.0f; break; default: case 2: sliceCount = 3.0f; samplesPerSliceSide = 3.0f; break; case 3: sliceCount = 9.0f; samplesPerSliceSide = 3.0f; break; } } WGPUShaderModule create_shader_module(const char* label, const ResourceBuffer& source) { WGPUShaderSourceWGSL wgsl = WGPU_SHADER_SOURCE_WGSL_INIT; wgsl.code = {static_cast(source.data), source.size}; WGPUShaderModuleDescriptor moduleDesc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT; moduleDesc.nextInChain = &wgsl.chain; moduleDesc.label = {label, WGPU_STRLEN}; return wgpuDeviceCreateShaderModule(g_deviceInfo.device, &moduleDesc); } bool build_compute_pipeline(const char* label, const ResourceBuffer& source, const char* entry, WGPUComputePipeline& outPipeline, WGPUBindGroupLayout& outLayout) { WGPUShaderModule module = create_shader_module(label, source); if (module == nullptr) { return false; } WGPUComputePipelineDescriptor pipelineDesc = WGPU_COMPUTE_PIPELINE_DESCRIPTOR_INIT; pipelineDesc.label = {label, WGPU_STRLEN}; pipelineDesc.compute.module = module; pipelineDesc.compute.entryPoint = {entry, WGPU_STRLEN}; outPipeline = wgpuDeviceCreateComputePipeline(g_deviceInfo.device, &pipelineDesc); wgpuShaderModuleRelease(module); if (outPipeline == nullptr) { return false; } outLayout = wgpuComputePipelineGetBindGroupLayout(outPipeline, 0); return outLayout != nullptr; } bool build_composite_pipeline( bool blend, WGPURenderPipeline& outPipeline, WGPUBindGroupLayout& outLayout) { WGPUShaderModule module = create_shader_module("AO composite", g_compositeSource); if (module == nullptr) { return false; } // Multiply blend WGPUBlendState blendState{ .color = { .operation = WGPUBlendOperation_Add, .srcFactor = WGPUBlendFactor_Dst, .dstFactor = WGPUBlendFactor_Zero, }, .alpha = { .operation = WGPUBlendOperation_Add, .srcFactor = WGPUBlendFactor_Zero, .dstFactor = WGPUBlendFactor_One, }, }; WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT; colorTarget.format = g_deviceInfo.color_format; if (blend) { colorTarget.blend = &blendState; } WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT; fragment.module = module; fragment.entryPoint = {"fs_main", WGPU_STRLEN}; fragment.targetCount = 1; fragment.targets = &colorTarget; // Depth state must match the EFB pass despite never touching depth. WGPUDepthStencilState depthStencil = WGPU_DEPTH_STENCIL_STATE_INIT; depthStencil.format = g_deviceInfo.depth_format; depthStencil.depthWriteEnabled = WGPUOptionalBool_False; depthStencil.depthCompare = WGPUCompareFunction_Always; WGPURenderPipelineDescriptor pipelineDesc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT; pipelineDesc.label = {blend ? "AO composite" : "AO composite (debug)", WGPU_STRLEN}; pipelineDesc.vertex.module = module; pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN}; pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList; pipelineDesc.depthStencil = &depthStencil; pipelineDesc.multisample.count = g_deviceInfo.sample_count; pipelineDesc.fragment = &fragment; outPipeline = wgpuDeviceCreateRenderPipeline(g_deviceInfo.device, &pipelineDesc); wgpuShaderModuleRelease(module); if (outPipeline == nullptr) { return false; } outLayout = wgpuRenderPipelineGetBindGroupLayout(outPipeline, 0); return outLayout != nullptr; } // Hilbert curve index LUT for the R2 noise sequence, generated once at init. // Ported from Bevy's generate_hilbert_index_lut (https://www.shadertoy.com/view/3tB3z3). uint16_t hilbert_index(uint16_t x, uint16_t y) { uint16_t index = 0; for (uint16_t level = 32; level > 0; level /= 2) { const uint16_t regionX = (x & level) > 0 ? 1 : 0; const uint16_t regionY = (y & level) > 0 ? 1 : 0; index += level * level * ((3 * regionX) ^ regionY); if (regionY == 0) { if (regionX == 1) { x = 63 - x; y = 63 - y; } std::swap(x, y); } } return index; } bool build_hilbert_lut() { WGPUTextureDescriptor texDesc = WGPU_TEXTURE_DESCRIPTOR_INIT; texDesc.label = {"AO hilbert LUT", WGPU_STRLEN}; texDesc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; texDesc.size = {64, 64, 1}; texDesc.format = WGPUTextureFormat_R16Uint; g_hilbertLut = wgpuDeviceCreateTexture(g_deviceInfo.device, &texDesc); if (g_hilbertLut == nullptr) { return false; } g_hilbertLutView = wgpuTextureCreateView(g_hilbertLut, nullptr); if (g_hilbertLutView == nullptr) { return false; } uint16_t lut[64 * 64]; for (uint16_t y = 0; y < 64; ++y) { for (uint16_t x = 0; x < 64; ++x) { lut[y * 64 + x] = hilbert_index(x, y); } } WGPUTexelCopyTextureInfo dst = WGPU_TEXEL_COPY_TEXTURE_INFO_INIT; dst.texture = g_hilbertLut; WGPUTexelCopyBufferLayout layout{.offset = 0, .bytesPerRow = 64 * 2, .rowsPerImage = 64}; WGPUExtent3D extent{64, 64, 1}; wgpuQueueWriteTexture(g_deviceInfo.queue, &dst, lut, sizeof(lut), &layout, &extent); return true; } void release_targets(AoTargets& targets) { for (auto*& view : targets.preprocessedDepthMips) { if (view != nullptr) { wgpuTextureViewRelease(view); view = nullptr; } } const auto releaseView = [](WGPUTextureView& view) { if (view != nullptr) { wgpuTextureViewRelease(view); view = nullptr; } }; const auto releaseTexture = [](WGPUTexture& texture) { if (texture != nullptr) { wgpuTextureRelease(texture); texture = nullptr; } }; releaseView(targets.preprocessedDepthAll); releaseView(targets.aoNoisyView); releaseView(targets.depthDifferencesView); releaseView(targets.aoFinalView); releaseTexture(targets.preprocessedDepth); releaseTexture(targets.aoNoisy); releaseTexture(targets.depthDifferences); releaseTexture(targets.aoFinal); targets.width = targets.height = 0; } void tick_retired_targets() { for (auto it = g_retiredTargets.begin(); it != g_retiredTargets.end();) { if (--it->framesLeft <= 0) { release_targets(it->targets); it = g_retiredTargets.erase(it); } else { ++it; } } } bool ensure_targets(uint32_t width, uint32_t height) { if (g_targets.width == width && g_targets.height == height) { return true; } if (g_targets.width != 0) { g_retiredTargets.push_back(RetiredTargets{std::exchange(g_targets, AoTargets{}), 4}); } const auto createStorageTexture = [&](const char* label, WGPUTextureFormat format, uint32_t mipCount, WGPUTexture& outTexture) { WGPUTextureDescriptor texDesc = WGPU_TEXTURE_DESCRIPTOR_INIT; texDesc.label = {label, WGPU_STRLEN}; texDesc.usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding; texDesc.size = {width, height, 1}; texDesc.format = format; texDesc.mipLevelCount = mipCount; outTexture = wgpuDeviceCreateTexture(g_deviceInfo.device, &texDesc); return outTexture != nullptr; }; bool ok = createStorageTexture("AO preprocessed depth", WGPUTextureFormat_R32Float, 5, g_targets.preprocessedDepth) && createStorageTexture("AO noisy", WGPUTextureFormat_R32Float, 1, g_targets.aoNoisy) && createStorageTexture("AO depth differences", WGPUTextureFormat_R32Uint, 1, g_targets.depthDifferences) && createStorageTexture("AO final", WGPUTextureFormat_R32Float, 1, g_targets.aoFinal); if (ok) { for (uint32_t mip = 0; mip < 5 && ok; ++mip) { WGPUTextureViewDescriptor viewDesc = WGPU_TEXTURE_VIEW_DESCRIPTOR_INIT; viewDesc.baseMipLevel = mip; viewDesc.mipLevelCount = 1; g_targets.preprocessedDepthMips[mip] = wgpuTextureCreateView(g_targets.preprocessedDepth, &viewDesc); ok = g_targets.preprocessedDepthMips[mip] != nullptr; } } if (ok) { g_targets.preprocessedDepthAll = wgpuTextureCreateView(g_targets.preprocessedDepth, nullptr); g_targets.aoNoisyView = wgpuTextureCreateView(g_targets.aoNoisy, nullptr); g_targets.depthDifferencesView = wgpuTextureCreateView(g_targets.depthDifferences, nullptr); g_targets.aoFinalView = wgpuTextureCreateView(g_targets.aoFinal, nullptr); ok = g_targets.preprocessedDepthAll != nullptr && g_targets.aoNoisyView != nullptr && g_targets.depthDifferencesView != nullptr && g_targets.aoFinalView != nullptr; } if (!ok) { release_targets(g_targets); return false; } g_targets.width = width; g_targets.height = height; return true; } constexpr uint32_t div_ceil(uint32_t numerator, uint32_t denominator) { return (numerator + denominator - 1) / denominator; } // Render worker thread: the AO chain as one compute pass with three dispatches. void on_compute( ModContext*, const GfxComputeContext* ctx, const void* payload, size_t payloadSize, void*) { if (payloadSize != sizeof(ComputePayload)) { return; } ComputePayload data; std::memcpy(&data, payload, sizeof(data)); if (data.depth == nullptr || g_preprocessPipeline == nullptr) { return; } const auto makeBindGroup = [&](WGPUBindGroupLayout layout, std::initializer_list entries) { WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT; bindGroupDesc.layout = layout; bindGroupDesc.entryCount = entries.size(); bindGroupDesc.entries = entries.begin(); return wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc); }; const auto textureEntry = [](uint32_t binding, WGPUTextureView view) { WGPUBindGroupEntry entry = WGPU_BIND_GROUP_ENTRY_INIT; entry.binding = binding; entry.textureView = view; return entry; }; const auto uniformEntry = [&](uint32_t binding) { WGPUBindGroupEntry entry = WGPU_BIND_GROUP_ENTRY_INIT; entry.binding = binding; entry.buffer = ctx->uniform_buffer; entry.offset = data.uniform_offset; entry.size = data.uniform_size; return entry; }; WGPUBindGroup preprocessGroup = makeBindGroup(g_preprocessLayout, {textureEntry(0, data.depth), textureEntry(1, data.preprocessedDepthMips[0]), textureEntry(2, data.preprocessedDepthMips[1]), textureEntry(3, data.preprocessedDepthMips[2]), textureEntry(4, data.preprocessedDepthMips[3]), uniformEntry(5)}); WGPUBindGroup mip4Group = makeBindGroup(g_mip4Layout, {textureEntry(6, data.preprocessedDepthMips[3]), textureEntry(7, data.preprocessedDepthMips[4])}); WGPUBindGroup gtaoGroup = makeBindGroup( g_gtaoLayout, {textureEntry(0, data.preprocessedDepthAll), textureEntry(1, g_hilbertLutView), textureEntry(2, data.aoNoisy), textureEntry(3, data.depthDifferences), uniformEntry(4)}); WGPUBindGroup denoiseGroup = makeBindGroup( g_denoiseLayout, {textureEntry(0, data.aoNoisy), textureEntry(1, data.depthDifferences), textureEntry(2, data.aoFinal), uniformEntry(3)}); if (preprocessGroup == nullptr || mip4Group == nullptr || gtaoGroup == nullptr || denoiseGroup == nullptr) { const auto release = [](WGPUBindGroup group) { if (group != nullptr) { wgpuBindGroupRelease(group); } }; release(preprocessGroup); release(mip4Group); release(gtaoGroup); release(denoiseGroup); return; } WGPUComputePassDescriptor passDesc = WGPU_COMPUTE_PASS_DESCRIPTOR_INIT; passDesc.label = {"AO chain", WGPU_STRLEN}; WGPUComputePassEncoder pass = wgpuCommandEncoderBeginComputePass(ctx->encoder, &passDesc); // Each preprocess workgroup covers 16x16 MIP-0 texels (8x8 invocations, 2x2 texels each). wgpuComputePassEncoderSetPipeline(pass, g_preprocessPipeline); wgpuComputePassEncoderSetBindGroup(pass, 0, preprocessGroup, 0, nullptr); wgpuComputePassEncoderDispatchWorkgroups( pass, div_ceil(data.width, 16), div_ceil(data.height, 16), 1); wgpuComputePassEncoderSetPipeline(pass, g_mip4Pipeline); wgpuComputePassEncoderSetBindGroup(pass, 0, mip4Group, 0, nullptr); wgpuComputePassEncoderDispatchWorkgroups(pass, div_ceil(std::max(data.width >> 4, 1u), 8), div_ceil(std::max(data.height >> 4, 1u), 8), 1); wgpuComputePassEncoderSetPipeline(pass, g_gtaoPipeline); wgpuComputePassEncoderSetBindGroup(pass, 0, gtaoGroup, 0, nullptr); wgpuComputePassEncoderDispatchWorkgroups( pass, div_ceil(data.width, 8), div_ceil(data.height, 8), 1); wgpuComputePassEncoderSetPipeline(pass, g_denoisePipeline); wgpuComputePassEncoderSetBindGroup(pass, 0, denoiseGroup, 0, nullptr); wgpuComputePassEncoderDispatchWorkgroups( pass, div_ceil(data.width, 8), div_ceil(data.height, 8), 1); wgpuComputePassEncoderEnd(pass); wgpuComputePassEncoderRelease(pass); wgpuBindGroupRelease(preprocessGroup); wgpuBindGroupRelease(mip4Group); wgpuBindGroupRelease(gtaoGroup); wgpuBindGroupRelease(denoiseGroup); g_chainExecuted.store(true, std::memory_order_release); } // Render worker thread: composite the AO over the scene (or show it, in debug view). void on_draw( ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) { if (payloadSize != sizeof(CompositePayload)) { return; } CompositePayload data; std::memcpy(&data, payload, sizeof(data)); WGPURenderPipeline pipeline = data.debug_view != 0 ? g_compositeDebugPipeline : g_compositePipeline; WGPUBindGroupLayout layout = data.debug_view != 0 ? g_compositeDebugLayout : g_compositeLayout; if (data.aoFinal == nullptr || data.preprocessedDepth == nullptr || data.sceneDepth == nullptr || 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.aoFinal; entries[1].binding = 1; entries[1].textureView = data.preprocessedDepth; entries[2].binding = 2; entries[2].textureView = data.sceneDepth; entries[3].binding = 3; entries[3].buffer = ctx->uniform_buffer; entries[3].offset = data.uniform_offset; entries[3].size = data.uniform_size; WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT; bindGroupDesc.layout = layout; bindGroupDesc.entryCount = 4; bindGroupDesc.entries = entries; WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc); if (bindGroup == nullptr) { return; } wgpuRenderPassEncoderSetPipeline(ctx->pass, pipeline); wgpuRenderPassEncoderSetBindGroup(ctx->pass, 0, bindGroup, 0, nullptr); wgpuRenderPassEncoderDraw(ctx->pass, 3, 1, 0, 0); wgpuBindGroupRelease(bindGroup); } // Game thread, after opaque scene draws and before translucent/fog overlay lists. void on_scene_after_opaque(ModContext*, const GfxStageContext* stageCtx, void*) { tick_retired_targets(); if (!get_bool_option(g_cvarEnabled, true)) { return; } if (stageCtx == nullptr || stageCtx->struct_size < sizeof(GfxStageContext) || stageCtx->game_view == nullptr) { return; } CameraInfo camera = CAMERA_INFO_INIT; if (svc_camera->get_camera(mod_ctx, stageCtx->game_view, &camera) != MOD_OK) { return; } GfxResolveDesc resolveDesc = GFX_RESOLVE_DESC_INIT; resolveDesc.color = false; resolveDesc.depth = true; GfxResolvedTargets resolved = GFX_RESOLVED_TARGETS_INIT; if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK || resolved.depth == nullptr) { if (!g_warnedNoDepth) { g_warnedNoDepth = true; svc_log->warn(mod_ctx, "depth snapshots unavailable; AO disabled"); } return; } const bool halfRes = get_bool_option(g_cvarHalfRes, false); const uint32_t divisor = halfRes ? 2 : 1; const uint32_t width = resolved.width / divisor; const uint32_t height = resolved.height / divisor; if (width < 32 || height < 32 || !ensure_targets(width, height)) { return; } AoUniforms uniforms{}; std::memcpy(uniforms.projection, camera.proj_from_view, sizeof(uniforms.projection)); std::memcpy( uniforms.inverse_projection, camera.view_from_proj, sizeof(uniforms.inverse_projection)); uniforms.size[0] = static_cast(width); uniforms.size[1] = static_cast(height); uniforms.inv_size[0] = 1.0f / uniforms.size[0]; uniforms.inv_size[1] = 1.0f / uniforms.size[1]; uniforms.depth_scale[0] = static_cast(resolved.width) / uniforms.size[0]; uniforms.depth_scale[1] = static_cast(resolved.height) / uniforms.size[1]; uniforms.effect_radius = static_cast(std::clamp(get_int_option(g_cvarRadius, 70), 10, 500)); uniforms.intensity = static_cast(std::clamp(get_int_option(g_cvarIntensity, 100), 0, 100)) / 100.0f; quality_counts( get_int_option(g_cvarQuality, 2), uniforms.slice_count, uniforms.samples_per_slice_side); const uint32_t debugMode = static_cast(std::clamp(get_int_option(g_cvarDebugView, 0), 0, 4)); uniforms.debug_view = debugMode; GfxRange uniformRange{0, 0}; if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &uniformRange) != MOD_OK) { return; } ComputePayload computePayload{}; computePayload.depth = resolved.depth; for (int mip = 0; mip < 5; ++mip) { computePayload.preprocessedDepthMips[mip] = g_targets.preprocessedDepthMips[mip]; } computePayload.preprocessedDepthAll = g_targets.preprocessedDepthAll; computePayload.aoNoisy = g_targets.aoNoisyView; computePayload.depthDifferences = g_targets.depthDifferencesView; computePayload.aoFinal = g_targets.aoFinalView; computePayload.uniform_offset = uniformRange.offset; computePayload.uniform_size = uniformRange.size; computePayload.width = width; computePayload.height = height; if (svc_gfx->push_compute(mod_ctx, g_computeType, &computePayload, sizeof(computePayload)) != MOD_OK) { return; } const CompositePayload drawPayload{g_targets.aoFinalView, g_targets.preprocessedDepthAll, resolved.depth, uniformRange.offset, uniformRange.size, debugMode}; svc_gfx->push_draw(mod_ctx, g_drawType, &drawPayload, sizeof(drawPayload)); } void add_control(UiElementHandle pane, const UiControlDesc& desc) { svc_ui->pane_add_control(mod_ctx, pane, &desc, nullptr); } void add_toggle(UiElementHandle pane, const char* label, ConfigVarHandle cvar, const char* help) { UiControlDesc control = UI_CONTROL_DESC_INIT; control.kind = UI_CONTROL_TOGGLE; control.label = label; control.help_rml = help; control.binding = UI_BINDING_CONFIG_VAR; control.config_var = cvar; add_control(pane, control); } ModResult build_controls_tab( ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) { (void)right; svc_ui->pane_add_section(mod_ctx, left, "Ambient Occlusion"); add_toggle(left, "Enabled", g_cvarEnabled, "Enables the GTAO pass."); static const char* kQualityOptions[] = {"Low", "Medium", "High", "Ultra"}; UiControlDesc control = UI_CONTROL_DESC_INIT; control.kind = UI_CONTROL_SELECT; control.label = "Quality"; control.help_rml = "Horizon slices and samples per pixel (XeGTAO presets: 4/8/18/54 spp)."; control.binding = UI_BINDING_CONFIG_VAR; control.config_var = g_cvarQuality; control.options = kQualityOptions; control.option_count = 4; add_control(left, control); control = UI_CONTROL_DESC_INIT; control.kind = UI_CONTROL_NUMBER; control.label = "Radius"; control.help_rml = "Occlusion sampling radius in world units."; control.binding = UI_BINDING_CONFIG_VAR; control.config_var = g_cvarRadius; control.min = 10; control.max = 500; control.step = 10; add_control(left, control); control = UI_CONTROL_DESC_INIT; control.kind = UI_CONTROL_NUMBER; control.label = "Intensity"; control.help_rml = "How strongly occlusion darkens the scene."; control.binding = UI_BINDING_CONFIG_VAR; control.config_var = g_cvarIntensity; control.min = 0; control.max = 100; control.step = 5; control.suffix = "%"; add_control(left, control); add_toggle(left, "Half Resolution", g_cvarHalfRes, "Computes AO at half resolution and upscales; faster, slightly softer."); static const char* kDebugOptions[] = {"Off", "AO", "Normals", "Depth", "Staircase"}; control = UI_CONTROL_DESC_INIT; control.kind = UI_CONTROL_SELECT; control.label = "Debug View"; control.help_rml = "AO: raw visibility as grayscale.
Normals: the view-space " "normals the GTAO pass consumes.
Depth: the preprocessed depth " "as a distance gradient.
Staircase: detects quantized depth - smooth " "depth is near-black with thin triangle edges, quantized depth lights " "up across surfaces."; control.binding = UI_BINDING_CONFIG_VAR; control.config_var = g_cvarDebugView; control.options = kDebugOptions; control.option_count = 5; add_control(left, control); return MOD_OK; } void on_controls_window_closed(ModContext*, UiWindowHandle, void*) { g_controlsWindow = 0; } void on_open_controls(ModContext*, void*) { if (g_controlsWindow != 0) { return; } UiTabDesc tabs[1] = {UI_TAB_DESC_INIT}; tabs[0].title = "Controls"; tabs[0].build = build_controls_tab; UiWindowDesc desc = UI_WINDOW_DESC_INIT; desc.tabs = tabs; desc.tab_count = 1; desc.on_closed = on_controls_window_closed; if (svc_ui->window_push(mod_ctx, &desc, &g_controlsWindow) != MOD_OK) { svc_log->error(mod_ctx, "failed to open AO controls window"); } } ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) { UiControlDesc control = UI_CONTROL_DESC_INIT; control.kind = UI_CONTROL_TOGGLE; control.label = "Enabled"; control.binding = UI_BINDING_CONFIG_VAR; control.config_var = g_cvarEnabled; add_control(panel, control); control = UI_CONTROL_DESC_INIT; control.kind = UI_CONTROL_BUTTON; control.label = "Open Controls"; control.on_pressed = on_open_controls; add_control(panel, control); return MOD_OK; } ModResult register_bool_option( const char* name, bool defaultValue, ConfigVarHandle& outHandle, ModError* error) { ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT; cvarDesc.name = name; cvarDesc.type = CONFIG_VAR_BOOL; cvarDesc.default_bool = defaultValue; if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to register AO option"); } return MOD_OK; } ModResult register_int_option( const char* name, int64_t defaultValue, ConfigVarHandle& outHandle, ModError* error) { ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT; cvarDesc.name = name; cvarDesc.type = CONFIG_VAR_INT; cvarDesc.default_int = defaultValue; if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to register AO option"); } return MOD_OK; } } // namespace extern "C" { MOD_EXPORT ModResult mod_initialize(ModError* error) { ModResult result = svc_resource->load(mod_ctx, "preprocess_depth.wgsl", &g_preprocessSource); if (result == MOD_OK) { result = svc_resource->load(mod_ctx, "gtao.wgsl", &g_gtaoSource); } if (result == MOD_OK) { result = svc_resource->load(mod_ctx, "denoise.wgsl", &g_denoiseSource); } if (result == MOD_OK) { result = svc_resource->load(mod_ctx, "composite.wgsl", &g_compositeSource); } if (result != MOD_OK) { return dusk::mods::set_error(error, result, "failed to load AO shaders"); } result = register_bool_option("effectEnabled", false, g_cvarEnabled, error); if (result != MOD_OK) { return result; } result = register_int_option("quality", 2, g_cvarQuality, error); if (result != MOD_OK) { return result; } result = register_int_option("radius", 70, g_cvarRadius, error); if (result != MOD_OK) { return result; } result = register_int_option("intensity", 100, g_cvarIntensity, error); if (result != MOD_OK) { return result; } result = register_bool_option("halfRes", false, g_cvarHalfRes, error); if (result != MOD_OK) { return result; } result = register_int_option("debugMode", 0, g_cvarDebugView, error); if (result != MOD_OK) { return result; } if (svc_gfx->get_device_info(mod_ctx, &g_deviceInfo) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to query device info"); } if (!build_compute_pipeline("AO preprocess depth", g_preprocessSource, "preprocess_depth", g_preprocessPipeline, g_preprocessLayout) || !build_compute_pipeline("AO downsample mip4", g_preprocessSource, "downsample_mip4", g_mip4Pipeline, g_mip4Layout) || !build_compute_pipeline("AO gtao", g_gtaoSource, "gtao", g_gtaoPipeline, g_gtaoLayout) || !build_compute_pipeline( "AO denoise", g_denoiseSource, "spatial_denoise", g_denoisePipeline, g_denoiseLayout)) { return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO compute pipelines"); } if (!build_composite_pipeline(true, g_compositePipeline, g_compositeLayout) || !build_composite_pipeline(false, g_compositeDebugPipeline, g_compositeDebugLayout)) { return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO composite pipeline"); } if (!build_hilbert_lut()) { return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO noise LUT"); } GfxComputeTypeDesc computeDesc = GFX_COMPUTE_TYPE_DESC_INIT; computeDesc.label = "AO chain"; computeDesc.callback = on_compute; if (svc_gfx->register_compute_type(mod_ctx, &computeDesc, &g_computeType) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to register compute type"); } GfxDrawTypeDesc drawDesc = GFX_DRAW_TYPE_DESC_INIT; drawDesc.label = "AO composite"; drawDesc.draw = on_draw; if (svc_gfx->register_draw_type(mod_ctx, &drawDesc, &g_drawType) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to register draw type"); } GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT; stageDesc.callback = on_scene_after_opaque; if (svc_gfx->register_stage_hook( mod_ctx, GFX_STAGE_SCENE_AFTER_OPAQUE, &stageDesc, &g_afterOpaqueHook) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to register stage hook"); } UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT; panelDesc.build = build_panel; svc_ui->register_mods_panel(mod_ctx, &panelDesc); svc_log->info(mod_ctx, "ao_mod ready"); return MOD_OK; } MOD_EXPORT ModResult mod_update(ModError*) { if (!g_loggedChain && g_chainExecuted.load(std::memory_order_acquire)) { g_loggedChain = true; svc_log->info(mod_ctx, "AO chain executed OK"); } return MOD_OK; } MOD_EXPORT ModResult mod_shutdown(ModError*) { svc_resource->free(mod_ctx, &g_preprocessSource); svc_resource->free(mod_ctx, &g_gtaoSource); svc_resource->free(mod_ctx, &g_denoiseSource); svc_resource->free(mod_ctx, &g_compositeSource); release_targets(g_targets); for (auto& retired : g_retiredTargets) { release_targets(retired.targets); } g_retiredTargets.clear(); const auto releasePipeline = [](WGPUComputePipeline& pipeline) { if (pipeline != nullptr) { wgpuComputePipelineRelease(pipeline); pipeline = nullptr; } }; const auto releaseLayout = [](WGPUBindGroupLayout& layout) { if (layout != nullptr) { wgpuBindGroupLayoutRelease(layout); layout = nullptr; } }; releasePipeline(g_preprocessPipeline); releasePipeline(g_mip4Pipeline); releasePipeline(g_gtaoPipeline); releasePipeline(g_denoisePipeline); releaseLayout(g_preprocessLayout); releaseLayout(g_mip4Layout); releaseLayout(g_gtaoLayout); releaseLayout(g_denoiseLayout); if (g_compositePipeline != nullptr) { wgpuRenderPipelineRelease(g_compositePipeline); g_compositePipeline = nullptr; } if (g_compositeDebugPipeline != nullptr) { wgpuRenderPipelineRelease(g_compositeDebugPipeline); g_compositeDebugPipeline = nullptr; } releaseLayout(g_compositeLayout); releaseLayout(g_compositeDebugLayout); if (g_hilbertLutView != nullptr) { wgpuTextureViewRelease(g_hilbertLutView); g_hilbertLutView = nullptr; } if (g_hilbertLut != nullptr) { wgpuTextureRelease(g_hilbertLut); g_hilbertLut = nullptr; } g_cvarEnabled = g_cvarQuality = g_cvarRadius = g_cvarIntensity = 0; g_cvarHalfRes = g_cvarDebugView = 0; g_computeType = g_drawType = 0; g_afterOpaqueHook = 0; g_controlsWindow = 0; return MOD_OK; } }