diff --git a/docs/modding.md b/docs/modding.md index 031f08c602..b2e7ba2e26 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -409,6 +409,35 @@ 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. +### GfxService (`mods/svc/gfx.h`) + +Direct WebGPU access at various stages of the rendering pipeline. Mods use the `wgpu*` C API (via `webgpu/webgpu.h`) for +custom draws and compute dispatches. Mods must manage their own WebGPU state, including pipelines and bind groups. + +```cpp +IMPORT_SERVICE(GfxService, svc_gfx); + +GfxDeviceInfo info = GFX_DEVICE_INFO_INIT; +svc_gfx->get_device_info(mod_ctx, &info); +``` + +`register_stage_hook` runs a game-thread callback during frame recording. The public stages are: + +- `GFX_STAGE_SCENE_BEGIN`: world camera window after camera/projection/light setup +- `GFX_STAGE_SCENE_AFTER_TERRAIN`: after terrain/shadow lists, before object and translucent lists +- `GFX_STAGE_SCENE_AFTER_OPAQUE`: after sky/terrain/object opaque lists, before translucent lists +- `GFX_STAGE_FRAME_BEFORE_HUD`: 3D scene and wipe are complete, before 2D/HUD lists +- `GFX_STAGE_FRAME_AFTER_HUD`: full game scene, including HUD + +Inside a stage callback, record work with `push_draw`, stream per-frame data with `push_verts`, `push_indices`, +`push_uniform`, or `push_storage`, snapshot the current frame with `resolve_pass`, and use `create_pass`/`resolve_pass` +for temporary offscreen passes. Draw callbacks run later on the render worker thread with the live +`WGPURenderPassEncoder`; they may use only their `GfxDrawContext` handles and raw `wgpu*` calls. Compute callbacks +registered with `register_compute_type` follow the same worker-thread rule and run on the frame command encoder. + +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`. + ### CameraService (`mods/svc/camera.h`) Converts a game view provided by a render callback into WebGPU-convention camera data. Matrix fields are column-major diff --git a/files.cmake b/files.cmake index 358d2373c1..c1546f1db0 100644 --- a/files.cmake +++ b/files.cmake @@ -1566,6 +1566,7 @@ set(DUSK_FILES src/dusk/mods/svc/config.cpp src/dusk/mods/svc/config.hpp src/dusk/mods/svc/game.cpp + src/dusk/mods/svc/gfx.cpp src/dusk/mods/svc/hook.cpp src/dusk/mods/svc/host.cpp src/dusk/mods/svc/log.cpp diff --git a/include/dusk/gfx.hpp b/include/dusk/gfx.hpp new file mode 100644 index 0000000000..adb04bee51 --- /dev/null +++ b/include/dusk/gfx.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "mods/svc/gfx.h" + +namespace dusk::mods { + +void gfx_run_stage(GfxStage stage, const view_class* gameView = nullptr, + const view_port_class* gameViewport = nullptr); + +} // namespace dusk::mods diff --git a/include/mods/svc/gfx.h b/include/mods/svc/gfx.h new file mode 100644 index 0000000000..a9812fd044 --- /dev/null +++ b/include/mods/svc/gfx.h @@ -0,0 +1,209 @@ +#pragma once + +#include "mods/api.h" + +#include + +/* + * Direct WebGPU access at various stages of the rendering pipeline. Mods use the wgpu* C API + * (via webgpu/webgpu.h) for custom draws and compute dispatches. + * + * Every service function must be called on the game thread. GfxStageFn callbacks run on the game + * thread during frame recording. push_draw, push_* and pass functions are valid from a stage + * callback and anywhere else GX commands are being recorded. + * + * GfxDrawFn and GfxComputeFn callbacks run on the render worker thread while the frame is encoded. + * They may use only the handles in their context struct and raw wgpu* calls; no other service may + * be called from them. + * + * All WGPU handles provided by this service are borrowed. Handles in callback contexts are valid + * only for the duration of the callback; views in GfxResolvedTargets are valid for the current + * frame only. GPU objects a mod creates through raw wgpu calls are its own responsibility and + * should be released in mod_shutdown. The device outlives all mods. + */ + +#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx" +#define GFX_SERVICE_MAJOR 1u +#define GFX_SERVICE_MINOR 0u + +/* Maximum size for push_draw payload */ +#define GFX_INLINE_DRAW_PAYLOAD_SIZE 128u + +/* 0 is never a valid handle. */ +typedef uint64_t GfxDrawTypeHandle; +typedef uint64_t GfxStageHookHandle; +typedef uint64_t GfxComputeTypeHandle; + +/* A suballocation in one of the shared per-frame streaming buffers. */ +typedef struct GfxRange { + uint32_t offset; + uint32_t size; +} GfxRange; + +/* + * Device and scene pass configuration. Valid from mod_initialize onward and stable for the + * session. Offscreen passes from create_pass are always single-sample. + */ +typedef struct GfxDeviceInfo { + uint32_t struct_size; + WGPUDevice device; /* borrowed */ + WGPUQueue queue; /* borrowed */ + WGPUTextureFormat color_format; /* scene color target format */ + 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 */ +} GfxDeviceInfo; + +#define GFX_DEVICE_INFO_INIT \ + {sizeof(GfxDeviceInfo), NULL, NULL, WGPUTextureFormat_Undefined, WGPUTextureFormat_Undefined, \ + 1u, false} + +/* + * Passed to GfxDrawFn on the render worker thread; valid only during the call. The pass pipeline, + * bind group, viewport, and scissor state is restored by the host after the callback returns. + */ +typedef struct GfxDrawContext { + uint32_t struct_size; + WGPUDevice device; + WGPUQueue queue; + WGPURenderPassEncoder pass; + WGPUBuffer vertex_buffer; + WGPUBuffer index_buffer; + WGPUBuffer uniform_buffer; + WGPUBuffer storage_buffer; + WGPUTextureFormat color_format; + WGPUTextureFormat depth_format; + uint32_t sample_count; + uint32_t target_width; + uint32_t target_height; + bool uses_reversed_z; +} GfxDrawContext; + +typedef void (*GfxDrawFn)(ModContext* ctx, const GfxDrawContext* draw_ctx, const void* payload, + size_t payload_size, void* user_data); + +typedef struct GfxDrawTypeDesc { + uint32_t struct_size; + const char* label; /* optional debug label */ + GfxDrawFn draw; /* required; called from the render worker thread */ + void* user_data; +} GfxDrawTypeDesc; + +#define GFX_DRAW_TYPE_DESC_INIT {sizeof(GfxDrawTypeDesc), NULL, NULL, NULL} + +typedef enum GfxStage { + GFX_STAGE_SCENE_AFTER_TERRAIN = 0, + GFX_STAGE_FRAME_BEFORE_HUD = 1, + GFX_STAGE_FRAME_AFTER_HUD = 2, + GFX_STAGE_SCENE_BEGIN = 3, + GFX_STAGE_SCENE_AFTER_OPAQUE = 4, +} GfxStage; + +typedef struct GfxStageContext { + uint32_t struct_size; + GfxStage stage; + const void* game_view; /* view_class* for world-camera stages; NULL otherwise */ + const void* game_viewport; /* view_port_class* for world-camera stages; NULL otherwise */ +} GfxStageContext; + +typedef void (*GfxStageFn)(ModContext* ctx, const GfxStageContext* stage_ctx, void* user_data); + +typedef struct GfxStageHookDesc { + uint32_t struct_size; + GfxStageFn callback; /* required */ + void* user_data; +} GfxStageHookDesc; + +#define GFX_STAGE_HOOK_DESC_INIT {sizeof(GfxStageHookDesc), NULL, NULL} + +typedef struct GfxResolveDesc { + uint32_t struct_size; + bool color; + bool depth; +} GfxResolveDesc; + +#define GFX_RESOLVE_DESC_INIT {sizeof(GfxResolveDesc), true, false} + +typedef struct GfxResolvedTargets { + uint32_t struct_size; + WGPUTextureView color; /* single-sample snapshot in color_format */ + WGPUTextureView depth; /* single-sample raw depth snapshot, R32Float when available */ + WGPUTextureFormat color_format; + uint32_t width; + uint32_t height; +} GfxResolvedTargets; + +#define GFX_RESOLVED_TARGETS_INIT \ + {sizeof(GfxResolvedTargets), NULL, NULL, WGPUTextureFormat_Undefined, 0u, 0u} + +/* + * Passed to GfxComputeFn on the render worker thread; valid only during the call. The encoder is + * the frame command encoder between scene render passes. Leave no pass open and never finish or + * release the encoder. + */ +typedef struct GfxComputeContext { + uint32_t struct_size; + WGPUDevice device; + WGPUQueue queue; + WGPUCommandEncoder encoder; + WGPUBuffer vertex_buffer; + WGPUBuffer index_buffer; + WGPUBuffer uniform_buffer; + WGPUBuffer storage_buffer; +} GfxComputeContext; + +typedef void (*GfxComputeFn)(ModContext* ctx, const GfxComputeContext* compute_ctx, + const void* payload, size_t payload_size, void* user_data); + +typedef struct GfxComputeTypeDesc { + uint32_t struct_size; + const char* label; /* optional debug label */ + GfxComputeFn callback; /* required; called from the render worker thread */ + void* user_data; +} GfxComputeTypeDesc; + +#define GFX_COMPUTE_TYPE_DESC_INIT {sizeof(GfxComputeTypeDesc), NULL, NULL, NULL} + +typedef struct GfxService { + ServiceHeader header; + + ModResult (*get_device_info)(ModContext* ctx, GfxDeviceInfo* out_info); + void* (*get_proc_address)(ModContext* ctx, const char* name); + + ModResult (*register_draw_type)( + ModContext* ctx, const GfxDrawTypeDesc* desc, GfxDrawTypeHandle* out_handle); + ModResult (*unregister_draw_type)(ModContext* ctx, GfxDrawTypeHandle handle); + ModResult (*push_draw)( + ModContext* ctx, GfxDrawTypeHandle handle, const void* payload, size_t payload_size); + + ModResult (*register_compute_type)( + ModContext* ctx, const GfxComputeTypeDesc* desc, GfxComputeTypeHandle* out_handle); + ModResult (*unregister_compute_type)(ModContext* ctx, GfxComputeTypeHandle handle); + ModResult (*push_compute)( + ModContext* ctx, GfxComputeTypeHandle handle, const void* payload, size_t payload_size); + + ModResult (*push_verts)( + ModContext* ctx, const void* data, size_t size, size_t alignment, GfxRange* out_range); + ModResult (*push_indices)( + ModContext* ctx, const void* data, size_t size, size_t alignment, GfxRange* out_range); + ModResult (*push_uniform)(ModContext* ctx, const void* data, size_t size, GfxRange* out_range); + ModResult (*push_storage)(ModContext* ctx, const void* data, size_t size, GfxRange* out_range); + + ModResult (*register_stage_hook)(ModContext* ctx, GfxStage stage, const GfxStageHookDesc* desc, + GfxStageHookHandle* out_handle); + ModResult (*unregister_stage_hook)(ModContext* ctx, GfxStageHookHandle handle); + + ModResult (*resolve_pass)( + ModContext* ctx, const GfxResolveDesc* desc, GfxResolvedTargets* out_targets); + ModResult (*create_pass)(ModContext* ctx, uint32_t width, uint32_t height); +} GfxService; + +#ifdef __cplusplus +#include "mods/service.hpp" + +template <> +struct dusk::mods::ServiceTraits { + static constexpr const char* id = GFX_SERVICE_ID; + static constexpr uint16_t major_version = GFX_SERVICE_MAJOR; +}; +#endif diff --git a/src/dusk/mods/svc/gfx.cpp b/src/dusk/mods/svc/gfx.cpp new file mode 100644 index 0000000000..7fa472454d --- /dev/null +++ b/src/dusk/mods/svc/gfx.cpp @@ -0,0 +1,846 @@ +#include "registry.hpp" + +#include "aurora/lib/logging.hpp" +#include "dusk/gfx.hpp" +#include "dusk/mods/loader/loader.hpp" +#include "mods/svc/gfx.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace dusk::mods { +namespace { + +aurora::Module Log("dusk::mods::gfx"); + +enum class GfxSlotKind : uint8_t { + DrawType, + StageHook, + ComputeType, +}; + +enum class GfxStreamBuffer : uint8_t { + Verts, + Indices, + Uniform, + Storage, +}; + +struct GfxSlot { + GfxSlotKind kind = GfxSlotKind::DrawType; + uint32_t generation = 1; + bool alive = false; + LoadedMod* owner = nullptr; + ModContext* ownerContext = nullptr; + std::string ownerId; + void* userData = nullptr; + + GfxDrawFn drawFn = nullptr; + aurora::gfx::DrawTypeId auroraDrawId = aurora::gfx::InvalidDrawType; + + GfxStageFn stageFn = nullptr; + GfxStage stage = GFX_STAGE_SCENE_AFTER_TERRAIN; + + GfxComputeFn computeFn = nullptr; + aurora::gfx::EncoderTaskId auroraTaskId = aurora::gfx::InvalidEncoderTask; +}; + +struct WorkerFailure { + std::string modId; + std::string message; + std::vector drawIds; + std::vector taskIds; +}; + +std::mutex s_mutex; +std::vector s_slots; +std::vector s_freeSlots; +std::vector s_workerFailures; +bool s_modOffscreenOpen = false; + +constexpr uint64_t make_handle(uint32_t index, uint32_t generation) { + return static_cast(generation) << 32 | index; +} + +GfxSlot* resolve_slot_locked(uint64_t handle, GfxSlotKind kind) { + const auto index = static_cast(handle & 0xFFFFFFFFu); + const auto generation = static_cast(handle >> 32); + if (handle == 0 || index >= s_slots.size()) { + return nullptr; + } + auto& slot = s_slots[index]; + if (!slot.alive || slot.generation != generation || slot.kind != kind) { + return nullptr; + } + return &slot; +} + +GfxSlot* resolve_owned_slot_locked(LoadedMod& mod, uint64_t handle, GfxSlotKind kind) { + auto* slot = resolve_slot_locked(handle, kind); + if (slot == nullptr || slot->owner != &mod) { + return nullptr; + } + return slot; +} + +uint32_t alloc_slot_locked() { + if (!s_freeSlots.empty()) { + const auto index = s_freeSlots.back(); + s_freeSlots.pop_back(); + return index; + } + const auto index = static_cast(s_slots.size()); + s_slots.emplace_back(); + return index; +} + +void free_slot_locked(uint32_t index) { + auto& slot = s_slots[index]; + slot.alive = false; + ++slot.generation; + slot.owner = nullptr; + slot.ownerContext = nullptr; + slot.ownerId.clear(); + slot.userData = nullptr; + slot.drawFn = nullptr; + slot.auroraDrawId = aurora::gfx::InvalidDrawType; + slot.stageFn = nullptr; + slot.stage = GFX_STAGE_SCENE_AFTER_TERRAIN; + slot.computeFn = nullptr; + slot.auroraTaskId = aurora::gfx::InvalidEncoderTask; + s_freeSlots.push_back(index); +} + +void collect_mod_slots_locked(LoadedMod* owner, std::vector& drawIds, + std::vector& taskIds) { + for (uint32_t i = 0; i < s_slots.size(); ++i) { + auto& slot = s_slots[i]; + if (!slot.alive || slot.owner != owner) { + continue; + } + if (slot.kind == GfxSlotKind::DrawType && slot.auroraDrawId != aurora::gfx::InvalidDrawType) + { + drawIds.push_back(slot.auroraDrawId); + } else if (slot.kind == GfxSlotKind::ComputeType && + slot.auroraTaskId != aurora::gfx::InvalidEncoderTask) + { + taskIds.push_back(slot.auroraTaskId); + } + free_slot_locked(i); + } +} + +void unregister_aurora_types(const std::vector& drawIds, + const std::vector& taskIds) { + for (const auto id : drawIds) { + aurora::gfx::unregister_draw_type(id); + } + for (const auto id : taskIds) { + aurora::gfx::unregister_encoder_task_type(id); + } +} + +void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPassEncoder& pass, + const void* payload, size_t payloadSize, void* userdata) { + const auto handle = static_cast(reinterpret_cast(userdata)); + GfxDrawFn fn = nullptr; + void* userData = nullptr; + ModContext* modContext = nullptr; + LoadedMod* owner = nullptr; + std::string ownerId; + { + std::lock_guard lock{s_mutex}; + auto* slot = resolve_slot_locked(handle, GfxSlotKind::DrawType); + if (slot == nullptr) { + return; + } + fn = slot->drawFn; + userData = slot->userData; + modContext = slot->ownerContext; + owner = slot->owner; + ownerId = slot->ownerId; + } + + GfxDrawContext drawContext{ + .struct_size = sizeof(GfxDrawContext), + .device = ctx.device.Get(), + .queue = ctx.queue.Get(), + .pass = pass.Get(), + .vertex_buffer = ctx.vertexBuffer.Get(), + .index_buffer = ctx.indexBuffer.Get(), + .uniform_buffer = ctx.uniformBuffer.Get(), + .storage_buffer = ctx.storageBuffer.Get(), + .color_format = static_cast(ctx.colorFormat), + .depth_format = static_cast(ctx.depthFormat), + .sample_count = ctx.sampleCount, + .target_width = ctx.targetWidth, + .target_height = ctx.targetHeight, + .uses_reversed_z = aurora::gfx::uses_reversed_z(), + }; + + std::string failure; + try { + fn(modContext, &drawContext, payload, payloadSize, userData); + return; + } catch (const std::exception& e) { + failure = fmt::format("exception in gfx draw callback: {}", e.what()); + } catch (...) { + failure = "unknown exception in gfx draw callback"; + } + + std::lock_guard lock{s_mutex}; + WorkerFailure record{ + .modId = std::move(ownerId), + .message = std::move(failure), + }; + collect_mod_slots_locked(owner, record.drawIds, record.taskIds); + s_workerFailures.push_back(std::move(record)); +} + +void compute_trampoline(const aurora::gfx::EncoderTaskContext& ctx, const wgpu::CommandEncoder& cmd, + const void* payload, size_t payloadSize, void* userdata) { + const auto handle = static_cast(reinterpret_cast(userdata)); + GfxComputeFn fn = nullptr; + void* userData = nullptr; + ModContext* modContext = nullptr; + LoadedMod* owner = nullptr; + std::string ownerId; + { + std::lock_guard lock{s_mutex}; + auto* slot = resolve_slot_locked(handle, GfxSlotKind::ComputeType); + if (slot == nullptr) { + return; + } + fn = slot->computeFn; + userData = slot->userData; + modContext = slot->ownerContext; + owner = slot->owner; + ownerId = slot->ownerId; + } + + GfxComputeContext computeContext{ + .struct_size = sizeof(GfxComputeContext), + .device = ctx.device.Get(), + .queue = ctx.queue.Get(), + .encoder = cmd.Get(), + .vertex_buffer = ctx.vertexBuffer.Get(), + .index_buffer = ctx.indexBuffer.Get(), + .uniform_buffer = ctx.uniformBuffer.Get(), + .storage_buffer = ctx.storageBuffer.Get(), + }; + + std::string failure; + try { + fn(modContext, &computeContext, payload, payloadSize, userData); + return; + } catch (const std::exception& e) { + failure = fmt::format("exception in gfx compute callback: {}", e.what()); + } catch (...) { + failure = "unknown exception in gfx compute callback"; + } + + std::lock_guard lock{s_mutex}; + WorkerFailure record{ + .modId = std::move(ownerId), + .message = std::move(failure), + }; + collect_mod_slots_locked(owner, record.drawIds, record.taskIds); + s_workerFailures.push_back(std::move(record)); +} + +} // namespace + +ModResult gfx_register_draw_type( + LoadedMod& mod, const char* label, GfxDrawFn draw, void* userData, uint64_t& outHandle) { + outHandle = 0; + + uint64_t handle = 0; + { + std::lock_guard lock{s_mutex}; + const auto index = alloc_slot_locked(); + auto& slot = s_slots[index]; + slot.kind = GfxSlotKind::DrawType; + slot.alive = true; + slot.owner = &mod; + slot.ownerContext = mod.context.get(); + slot.ownerId = mod.metadata.id; + slot.userData = userData; + slot.drawFn = draw; + handle = make_handle(index, slot.generation); + } + + const auto auroraId = aurora::gfx::register_draw_type(aurora::gfx::DrawTypeDescriptor{ + .label = label, + .draw = draw_trampoline, + .userdata = reinterpret_cast(static_cast(handle)), + }); + if (auroraId == aurora::gfx::InvalidDrawType) { + std::lock_guard lock{s_mutex}; + if (resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType) != nullptr) { + free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); + } + return MOD_ERROR; + } + + { + std::lock_guard lock{s_mutex}; + if (auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType)) { + slot->auroraDrawId = auroraId; + } + } + outHandle = handle; + return MOD_OK; +} + +ModResult gfx_unregister_draw_type(LoadedMod& mod, uint64_t handle) { + aurora::gfx::DrawTypeId auroraId = aurora::gfx::InvalidDrawType; + { + std::lock_guard lock{s_mutex}; + auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auroraId = slot->auroraDrawId; + free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); + } + aurora::gfx::unregister_draw_type(auroraId); + return MOD_OK; +} + +ModResult gfx_push_draw(LoadedMod& mod, uint64_t handle, const void* payload, size_t payloadSize) { + aurora::gfx::DrawTypeId auroraId = aurora::gfx::InvalidDrawType; + { + std::lock_guard lock{s_mutex}; + auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auroraId = slot->auroraDrawId; + } + if (!aurora::gfx::push_custom_draw(auroraId, payload, payloadSize)) { + return MOD_UNAVAILABLE; + } + return MOD_OK; +} + +ModResult gfx_push_stream( + GfxStreamBuffer buffer, const void* data, size_t size, size_t alignment, GfxRange& outRange) { + aurora::gfx::Range range; + const auto* bytes = static_cast(data); + switch (buffer) { + case GfxStreamBuffer::Verts: + range = aurora::gfx::push_verts(bytes, size, alignment); + break; + case GfxStreamBuffer::Indices: + range = aurora::gfx::push_indices(bytes, size, alignment); + break; + case GfxStreamBuffer::Uniform: + range = aurora::gfx::push_uniform(bytes, size); + break; + case GfxStreamBuffer::Storage: + range = aurora::gfx::push_storage(bytes, size); + break; + } + if (range.size == 0) { + return MOD_UNAVAILABLE; + } + outRange = GfxRange{.offset = range.offset, .size = range.size}; + return MOD_OK; +} + +ModResult gfx_register_stage_hook( + LoadedMod& mod, GfxStage stage, GfxStageFn callback, void* userData, uint64_t& outHandle) { + outHandle = 0; + std::lock_guard lock{s_mutex}; + const auto index = alloc_slot_locked(); + auto& slot = s_slots[index]; + slot.kind = GfxSlotKind::StageHook; + slot.alive = true; + slot.owner = &mod; + slot.ownerContext = mod.context.get(); + slot.ownerId = mod.metadata.id; + slot.userData = userData; + slot.stageFn = callback; + slot.stage = stage; + outHandle = make_handle(index, slot.generation); + return MOD_OK; +} + +ModResult gfx_unregister_stage_hook(LoadedMod& mod, uint64_t handle) { + std::lock_guard lock{s_mutex}; + auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::StageHook); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); + return MOD_OK; +} + +ModResult gfx_resolve_pass(LoadedMod& mod, const GfxResolveDesc& desc, GfxResolvedTargets& out) { + out = GfxResolvedTargets{.struct_size = sizeof(GfxResolvedTargets)}; + if (aurora::gfx::is_offscreen() && !s_modOffscreenOpen) { + Log.error( + "[{}] resolve_pass: the active offscreen pass belongs to the game", mod.metadata.id); + return MOD_UNAVAILABLE; + } + const bool closesModOffscreen = s_modOffscreenOpen; + + aurora::gfx::ResolvedTargets resolved; + if (!aurora::gfx::resolve_pass( + aurora::gfx::ResolveDesc{.color = desc.color, .depth = desc.depth}, resolved)) + { + return MOD_UNAVAILABLE; + } + if (closesModOffscreen) { + s_modOffscreenOpen = false; + } + + out.color = resolved.color.Get(); + out.depth = resolved.depth.Get(); + out.color_format = static_cast(resolved.colorFormat); + out.width = resolved.width; + out.height = resolved.height; + return MOD_OK; +} + +ModResult gfx_create_pass(LoadedMod& mod, uint32_t width, uint32_t height) { + if (aurora::gfx::is_offscreen()) { + Log.error("[{}] create_pass: an offscreen pass is already active", mod.metadata.id); + return MOD_UNAVAILABLE; + } + if (!aurora::gfx::create_pass(width, height)) { + return MOD_UNAVAILABLE; + } + s_modOffscreenOpen = true; + return MOD_OK; +} + +ModResult gfx_register_compute_type( + LoadedMod& mod, const char* label, GfxComputeFn callback, void* userData, uint64_t& outHandle) { + outHandle = 0; + + uint64_t handle = 0; + { + std::lock_guard lock{s_mutex}; + const auto index = alloc_slot_locked(); + auto& slot = s_slots[index]; + slot.kind = GfxSlotKind::ComputeType; + slot.alive = true; + slot.owner = &mod; + slot.ownerContext = mod.context.get(); + slot.ownerId = mod.metadata.id; + slot.userData = userData; + slot.computeFn = callback; + handle = make_handle(index, slot.generation); + } + + const auto auroraId = + aurora::gfx::register_encoder_task_type(aurora::gfx::EncoderTaskDescriptor{ + .label = label, + .callback = compute_trampoline, + .userdata = reinterpret_cast(static_cast(handle)), + }); + if (auroraId == aurora::gfx::InvalidEncoderTask) { + std::lock_guard lock{s_mutex}; + if (resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType) != nullptr) { + free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); + } + return MOD_ERROR; + } + + { + std::lock_guard lock{s_mutex}; + if (auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType)) { + slot->auroraTaskId = auroraId; + } + } + outHandle = handle; + return MOD_OK; +} + +ModResult gfx_unregister_compute_type(LoadedMod& mod, uint64_t handle) { + aurora::gfx::EncoderTaskId auroraId = aurora::gfx::InvalidEncoderTask; + { + std::lock_guard lock{s_mutex}; + auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auroraId = slot->auroraTaskId; + free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); + } + aurora::gfx::unregister_encoder_task_type(auroraId); + return MOD_OK; +} + +ModResult gfx_push_compute( + LoadedMod& mod, uint64_t handle, const void* payload, size_t payloadSize) { + aurora::gfx::EncoderTaskId auroraId = aurora::gfx::InvalidEncoderTask; + { + std::lock_guard lock{s_mutex}; + auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auroraId = slot->auroraTaskId; + } + if (!aurora::gfx::push_encoder_task(auroraId, payload, payloadSize)) { + return MOD_UNAVAILABLE; + } + return MOD_OK; +} + +void gfx_run_stage( + GfxStage stage, const view_class* gameView, const view_port_class* gameViewport) { + struct StageEntry { + uint64_t handle; + GfxStageFn fn; + void* userData; + ModContext* context; + LoadedMod* owner; + }; + + std::vector entries; + { + std::lock_guard lock{s_mutex}; + for (uint32_t i = 0; i < s_slots.size(); ++i) { + const auto& slot = s_slots[i]; + if (slot.alive && slot.kind == GfxSlotKind::StageHook && slot.stage == stage) { + entries.push_back(StageEntry{ + .handle = make_handle(i, slot.generation), + .fn = slot.stageFn, + .userData = slot.userData, + .context = slot.ownerContext, + .owner = slot.owner, + }); + } + } + } + if (entries.empty()) { + return; + } + + const GfxStageContext stageContext{ + .struct_size = sizeof(GfxStageContext), + .stage = stage, + .game_view = gameView, + .game_viewport = gameViewport, + }; + + for (const auto& entry : entries) { + { + std::lock_guard lock{s_mutex}; + if (resolve_slot_locked(entry.handle, GfxSlotKind::StageHook) == nullptr) { + continue; + } + } + if (!entry.owner->active) { + continue; + } + + const bool wasOffscreen = aurora::gfx::is_offscreen(); + try { + entry.fn(entry.context, &stageContext, entry.userData); + } catch (const std::exception& e) { + fail_mod(*entry.owner, MOD_ERROR, + fmt::format("exception in gfx stage callback: {}", e.what())); + } catch (...) { + fail_mod(*entry.owner, MOD_ERROR, "unknown exception in gfx stage callback"); + } + + if (aurora::gfx::is_offscreen() != wasOffscreen) { + aurora::gfx::ResolvedTargets discarded; + aurora::gfx::resolve_pass( + aurora::gfx::ResolveDesc{.color = false, .depth = false}, discarded); + s_modOffscreenOpen = false; + fail_mod(*entry.owner, MOD_ERROR, + "gfx stage callback returned with its offscreen pass still open"); + } + } +} + +void gfx_drain_worker_failures() { + std::vector failures; + { + std::lock_guard lock{s_mutex}; + failures.swap(s_workerFailures); + } + if (failures.empty()) { + return; + } + + bool needsSynchronize = false; + for (const auto& failure : failures) { + unregister_aurora_types(failure.drawIds, failure.taskIds); + needsSynchronize = needsSynchronize || !failure.drawIds.empty() || !failure.taskIds.empty(); + } + if (needsSynchronize) { + aurora::gfx::synchronize(); + } + + for (const auto& failure : failures) { + for (auto& mod : ModLoader::instance().mods()) { + if (mod.metadata.id == failure.modId && mod.active) { + fail_mod(mod, MOD_ERROR, failure.message); + break; + } + } + } +} + +void gfx_remove_mod(LoadedMod& mod) { + std::vector drawIds; + std::vector taskIds; + { + std::lock_guard lock{s_mutex}; + collect_mod_slots_locked(&mod, drawIds, taskIds); + } + if (drawIds.empty() && taskIds.empty()) { + return; + } + unregister_aurora_types(drawIds, taskIds); + aurora::gfx::synchronize(); +} + +} // namespace dusk::mods + +namespace dusk::mods::svc { +namespace { + +ModResult gfx_get_device_info(ModContext* context, GfxDeviceInfo* outInfo) { + if (outInfo == nullptr || outInfo->struct_size < sizeof(GfxDeviceInfo)) { + return MOD_INVALID_ARGUMENT; + } + const uint32_t structSize = outInfo->struct_size; + *outInfo = GfxDeviceInfo{.struct_size = structSize}; + + auto* mod = mod_from_context(context); + if (mod == nullptr) { + return MOD_INVALID_ARGUMENT; + } + + outInfo->device = aurora::gfx::device().Get(); + outInfo->queue = aurora::gfx::queue().Get(); + outInfo->color_format = static_cast(aurora::gfx::color_format()); + outInfo->depth_format = static_cast(aurora::gfx::depth_format()); + outInfo->sample_count = aurora::gfx::sample_count(); + outInfo->uses_reversed_z = aurora::gfx::uses_reversed_z(); + return MOD_OK; +} + +void* gfx_get_proc_address(ModContext* context, const char* name) { + if (mod_from_context(context) == nullptr || name == nullptr) { + return nullptr; + } + return reinterpret_cast(wgpuGetProcAddress(WGPUStringView{name, WGPU_STRLEN})); +} + +ModResult gfx_register_draw_type_impl( + ModContext* context, const GfxDrawTypeDesc* desc, GfxDrawTypeHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxDrawTypeDesc) || + desc->draw == nullptr || outHandle == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + uint64_t handle = 0; + const auto result = + gfx_register_draw_type(*mod, desc->label, desc->draw, desc->user_data, handle); + if (result != MOD_OK) { + return result; + } + *outHandle = handle; + return MOD_OK; +} + +ModResult gfx_unregister_draw_type_impl(ModContext* context, GfxDrawTypeHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return gfx_unregister_draw_type(*mod, handle); +} + +ModResult gfx_push_draw_impl( + ModContext* context, GfxDrawTypeHandle handle, const void* payload, size_t payloadSize) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE || + (payloadSize > 0 && payload == nullptr)) + { + return MOD_INVALID_ARGUMENT; + } + return gfx_push_draw(*mod, handle, payload, payloadSize); +} + +ModResult gfx_push_stream_impl(ModContext* context, GfxStreamBuffer buffer, const void* data, + size_t size, size_t alignment, GfxRange* outRange) { + if (outRange != nullptr) { + *outRange = GfxRange{0, 0}; + } + if (mod_from_context(context) == nullptr || data == nullptr || size == 0 || outRange == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + return gfx_push_stream(buffer, data, size, alignment, *outRange); +} + +ModResult gfx_push_verts_impl( + ModContext* context, const void* data, size_t size, size_t alignment, GfxRange* outRange) { + return gfx_push_stream_impl(context, GfxStreamBuffer::Verts, data, size, alignment, outRange); +} + +ModResult gfx_push_indices_impl( + ModContext* context, const void* data, size_t size, size_t alignment, GfxRange* outRange) { + return gfx_push_stream_impl(context, GfxStreamBuffer::Indices, data, size, alignment, outRange); +} + +ModResult gfx_push_uniform_impl( + ModContext* context, const void* data, size_t size, GfxRange* outRange) { + return gfx_push_stream_impl(context, GfxStreamBuffer::Uniform, data, size, 0, outRange); +} + +ModResult gfx_push_storage_impl( + ModContext* context, const void* data, size_t size, GfxRange* outRange) { + return gfx_push_stream_impl(context, GfxStreamBuffer::Storage, data, size, 0, outRange); +} + +bool valid_stage(GfxStage stage) { + return stage == GFX_STAGE_SCENE_AFTER_TERRAIN || stage == GFX_STAGE_FRAME_BEFORE_HUD || + stage == GFX_STAGE_FRAME_AFTER_HUD || stage == GFX_STAGE_SCENE_BEGIN || + stage == GFX_STAGE_SCENE_AFTER_OPAQUE; +} + +ModResult gfx_register_stage_hook_impl(ModContext* context, GfxStage stage, + const GfxStageHookDesc* desc, GfxStageHookHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxStageHookDesc) || + desc->callback == nullptr || outHandle == nullptr || !valid_stage(stage)) + { + return MOD_INVALID_ARGUMENT; + } + uint64_t handle = 0; + const auto result = + gfx_register_stage_hook(*mod, stage, desc->callback, desc->user_data, handle); + if (result != MOD_OK) { + return result; + } + *outHandle = handle; + return MOD_OK; +} + +ModResult gfx_unregister_stage_hook_impl(ModContext* context, GfxStageHookHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return gfx_unregister_stage_hook(*mod, handle); +} + +ModResult gfx_resolve_pass_impl( + ModContext* context, const GfxResolveDesc* desc, GfxResolvedTargets* outTargets) { + if (outTargets != nullptr && outTargets->struct_size >= sizeof(GfxResolvedTargets)) { + *outTargets = GfxResolvedTargets{.struct_size = sizeof(GfxResolvedTargets)}; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxResolveDesc) || + outTargets == nullptr || outTargets->struct_size < sizeof(GfxResolvedTargets) || + (!desc->color && !desc->depth)) + { + return MOD_INVALID_ARGUMENT; + } + return gfx_resolve_pass(*mod, *desc, *outTargets); +} + +ModResult gfx_create_pass_impl(ModContext* context, uint32_t width, uint32_t height) { + auto* mod = mod_from_context(context); + if (mod == nullptr || width == 0 || height == 0) { + return MOD_INVALID_ARGUMENT; + } + return gfx_create_pass(*mod, width, height); +} + +ModResult gfx_register_compute_type_impl( + ModContext* context, const GfxComputeTypeDesc* desc, GfxComputeTypeHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxComputeTypeDesc) || + desc->callback == nullptr || outHandle == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + uint64_t handle = 0; + const auto result = + gfx_register_compute_type(*mod, desc->label, desc->callback, desc->user_data, handle); + if (result != MOD_OK) { + return result; + } + *outHandle = handle; + return MOD_OK; +} + +ModResult gfx_unregister_compute_type_impl(ModContext* context, GfxComputeTypeHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return gfx_unregister_compute_type(*mod, handle); +} + +ModResult gfx_push_compute_impl( + ModContext* context, GfxComputeTypeHandle handle, const void* payload, size_t payloadSize) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE || + (payloadSize > 0 && payload == nullptr)) + { + return MOD_INVALID_ARGUMENT; + } + return gfx_push_compute(*mod, handle, payload, payloadSize); +} + +constexpr GfxService s_gfxService{ + .header = SERVICE_HEADER(GfxService, GFX_SERVICE_MAJOR, GFX_SERVICE_MINOR), + .get_device_info = gfx_get_device_info, + .get_proc_address = gfx_get_proc_address, + .register_draw_type = gfx_register_draw_type_impl, + .unregister_draw_type = gfx_unregister_draw_type_impl, + .push_draw = gfx_push_draw_impl, + .register_compute_type = gfx_register_compute_type_impl, + .unregister_compute_type = gfx_unregister_compute_type_impl, + .push_compute = gfx_push_compute_impl, + .push_verts = gfx_push_verts_impl, + .push_indices = gfx_push_indices_impl, + .push_uniform = gfx_push_uniform_impl, + .push_storage = gfx_push_storage_impl, + .register_stage_hook = gfx_register_stage_hook_impl, + .unregister_stage_hook = gfx_unregister_stage_hook_impl, + .resolve_pass = gfx_resolve_pass_impl, + .create_pass = gfx_create_pass_impl, +}; + +} // namespace + +constinit const ServiceModule g_gfxModule{ + .id = GFX_SERVICE_ID, + .majorVersion = GFX_SERVICE_MAJOR, + .minorVersion = GFX_SERVICE_MINOR, + .service = &s_gfxService, + .modDetached = gfx_remove_mod, + .frameBegin = gfx_drain_worker_failures, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index f9e7f96476..338625230a 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -209,6 +209,7 @@ void ModLoader::init_services() { &svc::g_uiModule, &svc::g_gameModule, &svc::g_cameraModule, + &svc::g_gfxModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index b4b76cd510..191626e0ef 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -71,5 +71,6 @@ extern const ServiceModule g_configModule; extern const ServiceModule g_uiModule; extern const ServiceModule g_gameModule; extern const ServiceModule g_cameraModule; +extern const ServiceModule g_gfxModule; } // namespace dusk::mods::svc diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index caf823b065..e7a5bf9b54 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -53,6 +53,7 @@ #include "dusk/dusk.h" #include "dusk/endian.h" #include "dusk/frame_interpolation.h" +#include "dusk/gfx.hpp" #include "dusk/gx_helper.h" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/logging.h" @@ -2356,6 +2357,10 @@ int mDoGph_Painter() { GXSetClipMode(GX_CLIP_ENABLE); +#if TARGET_PC + dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_BEGIN, &camera_p->view, view_port); +#endif + #if DEBUG // "drawing up to Background (Translucent) (Rendering)" fapGm_HIO_c::stopCpuTimer("背景(半透明)描画まで(レンダリング)"); @@ -2384,6 +2389,10 @@ int mDoGph_Painter() { GX_DEBUG_GROUP(dComIfGd_drawShadow, camera_p->view.viewMtx); +#if TARGET_PC + dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_AFTER_TERRAIN, &camera_p->view, view_port); +#endif + #if DEBUG // "shadow drawing (Rendering)" fapGm_HIO_c::stopCpuTimer("影描画(レンダリング)"); @@ -2413,6 +2422,10 @@ int mDoGph_Painter() { GX_DEBUG_GROUP(dComIfGd_drawOpaListPacket); +#if TARGET_PC + dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_AFTER_OPAQUE, &camera_p->view, view_port); +#endif + #if DEBUG // "drawing up to special-use drawing (Opaque) except J3D (Rendering)" fapGm_HIO_c::stopCpuTimer("J3D以外などの特殊用(不透明)描画まで(レンダリング)"); @@ -2778,6 +2791,10 @@ int mDoGph_Painter() { captureScreenSetPort(); #endif +#if TARGET_PC + dusk::mods::gfx_run_stage(GFX_STAGE_FRAME_BEFORE_HUD); +#endif + if (fapGmHIO_get2Ddraw()) { Mtx m4; cMtx_copy(j3dSys.getViewMtx(), m4); @@ -2835,6 +2852,10 @@ int mDoGph_Painter() { dComIfGd_draw2DXlu(); } +#if TARGET_PC + dusk::mods::gfx_run_stage(GFX_STAGE_FRAME_AFTER_HUD); +#endif + #if DEBUG if (dJcame_c::get()) { dJcame_c::get()->show2D();