Mod API: gfx service

This commit is contained in:
Luke Street
2026-07-09 23:08:59 -06:00
parent 7df07c6904
commit d9a978f21f
8 changed files with 1118 additions and 0 deletions
+29
View File
@@ -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
+1
View File
@@ -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
+10
View File
@@ -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
+209
View File
@@ -0,0 +1,209 @@
#pragma once
#include "mods/api.h"
#include <webgpu/webgpu.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.
*
* 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<GfxService> {
static constexpr const char* id = GFX_SERVICE_ID;
static constexpr uint16_t major_version = GFX_SERVICE_MAJOR;
};
#endif
+846
View File
@@ -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 <aurora/gfx.hpp>
#include <fmt/format.h>
#include <cstdint>
#include <exception>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
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<aurora::gfx::DrawTypeId> drawIds;
std::vector<aurora::gfx::EncoderTaskId> taskIds;
};
std::mutex s_mutex;
std::vector<GfxSlot> s_slots;
std::vector<uint32_t> s_freeSlots;
std::vector<WorkerFailure> s_workerFailures;
bool s_modOffscreenOpen = false;
constexpr uint64_t make_handle(uint32_t index, uint32_t generation) {
return static_cast<uint64_t>(generation) << 32 | index;
}
GfxSlot* resolve_slot_locked(uint64_t handle, GfxSlotKind kind) {
const auto index = static_cast<uint32_t>(handle & 0xFFFFFFFFu);
const auto generation = static_cast<uint32_t>(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<uint32_t>(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<aurora::gfx::DrawTypeId>& drawIds,
std::vector<aurora::gfx::EncoderTaskId>& 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<aurora::gfx::DrawTypeId>& drawIds,
const std::vector<aurora::gfx::EncoderTaskId>& 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<uint64_t>(reinterpret_cast<uintptr_t>(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<WGPUTextureFormat>(ctx.colorFormat),
.depth_format = static_cast<WGPUTextureFormat>(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<uint64_t>(reinterpret_cast<uintptr_t>(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<void*>(static_cast<uintptr_t>(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<uint32_t>(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<uint32_t>(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<const uint8_t*>(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<uint32_t>(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<WGPUTextureFormat>(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<void*>(static_cast<uintptr_t>(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<uint32_t>(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<uint32_t>(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<StageEntry> 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<WorkerFailure> 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<aurora::gfx::DrawTypeId> drawIds;
std::vector<aurora::gfx::EncoderTaskId> 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<WGPUTextureFormat>(aurora::gfx::color_format());
outInfo->depth_format = static_cast<WGPUTextureFormat>(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<void*>(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
+1
View File
@@ -209,6 +209,7 @@ void ModLoader::init_services() {
&svc::g_uiModule,
&svc::g_gameModule,
&svc::g_cameraModule,
&svc::g_gfxModule,
})
{
svc::register_module(*module);
+1
View File
@@ -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
+21
View File
@@ -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();