diff --git a/files.cmake b/files.cmake index 0834dc9f0d..18b44daa59 100644 --- a/files.cmake +++ b/files.cmake @@ -1538,6 +1538,7 @@ set(DUSK_FILES src/dusk/mods/svc/item.hpp src/dusk/mods/svc/id_allocator.cpp src/dusk/mods/svc/id_allocator.hpp + src/dusk/mods/svc/interp.cpp src/dusk/mods/svc/log.cpp src/dusk/mods/svc/overlay.cpp src/dusk/mods/svc/registry.cpp diff --git a/sdk/include/mods/svc/interp.h b/sdk/include/mods/svc/interp.h new file mode 100644 index 0000000000..2b7e595840 --- /dev/null +++ b/sdk/include/mods/svc/interp.h @@ -0,0 +1,85 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define INTERP_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "interp" +#define INTERP_SERVICE_MAJOR 1u +#define INTERP_SERVICE_MINOR 0u + +// This is the same type as Mtx in the Dolphin types. +/** + * A row-major 3x4 (3 rows, 4 columns) matrix for usage in the interpolation system. + */ +typedef float InterpMtx[3][4]; + +/** + * Defines APIs for interpolating render data between multiple simulation frames. + */ +typedef struct InterpService { + ServiceHeader header; + + /* + * Matrix API. + * + * Using this API is pretty simple: you "record" a matrix during sim frames, + * then later look up the interpolated "replacement" matrix during rendering. + * + * A matrix is identified between frames via an arbitrary pointer. While this is typically + * the address of the matrix itself, it can be any unique pointer-sized value. + * You should "unregister" a matrix key with @ref forget_mtx when you are done with it. + * + * Note: when working with actors, the draw callback is called from simulation. + * You should access the interpolated matrices from the packet callback instead. + */ + + /** + * Record a matrix for interpolation, with an arbitrary key to identify it. + * + * @remarks Recorded keys should be unregistered with @ref forget_mtx to avoid memory leaks. + * + * @param mod Your mod's context. + * @param matrix The matrix value to record. + * @param key The key value used to identify the recorded matrix. Cannot be null. + */ + ModResult (*record_mtx_keyed)(ModContext* mod, InterpMtx matrix, void const* key); + + /** + * Record a matrix for interpolation. + * + * @remarks This is effectively equivalent to calling @ref record_mtx_keyed with the matrix + * as both arguments. + * @remarks Recorded keys should be unregistered with @ref forget_mtx to avoid memory leaks. + * + * @param mod Your mod's context. + * @param matrix The matrix value to record. Its address is also used as a key to identify it. Cannot be null. + */ + ModResult (*record_mtx)(ModContext* mod, InterpMtx matrix); + + /** + * Forgets an interpolated matrix that was previously recorded. + * + * @remarks This API does not cause an error if the key is not known. + */ + ModResult (*forget_mtx)(ModContext* mod, void const* key); + + /** + * Look up the interpolated replacement for a matrix that was previously recorded. + * + * @remarks It is legal to look up replacements for matrices that were registered by other + * mods or base game code. + * + * @param key Key to look up the replacement for. + * @param out Pointer that will receive the interpolated replacement matrix. + * Not written if the operation fails. + * + * @returns false if the matrix is unknown. + */ + bool (*lookup_replacement_mtx)(void const* key, InterpMtx out); +} InterpService; + +MOD_DECLARE_SERVICE(InterpService, svc_interp, INTERP_SERVICE_ID, INTERP_SERVICE_MAJOR, + INTERP_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/interp.hpp b/sdk/include/mods/svc/interp.hpp new file mode 100644 index 0000000000..51f56a1bdf --- /dev/null +++ b/sdk/include/mods/svc/interp.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include "interp.h" + +namespace mods::interp { + +/** + * RAII wrapper for interpolation matrices. + * + * @remarks This is a simple wrapper around a @ref InterpMtx. + * The destructor automatically calls @ref forget_mtx, while the assignment operators + * automatically call @ref record. + */ +struct InterpMatrix { + InterpMtx mtx{}; + + InterpMatrix() = default; + + explicit(false) InterpMatrix(InterpMtx const mtx) { + std::memcpy(&this->mtx, mtx, sizeof(this->mtx)); + } + + InterpMatrix(InterpMatrix const& src) : InterpMatrix(src.mtx) {} + + InterpMatrix& operator=(InterpMatrix const& other) { return operator=(other.mtx); } + + InterpMatrix& operator=(InterpMtx const other) { + std::memcpy(&this->mtx, other, sizeof(this->mtx)); + record(); + return *this; + } + + /** + * Record the current value of the matrix. + */ + void record() { svc_interp->record_mtx(mod_ctx, mtx); } + + /** + * Read the interpolated value of the matrix. + */ + void readInterpolated(InterpMtx& out) const { + if (!svc_interp->lookup_replacement_mtx(&mtx, out)) { + std::memcpy(out, mtx, sizeof(mtx)); + } + } + + ~InterpMatrix() { svc_interp->forget_mtx(mod_ctx, &mtx); } +}; + +} // namespace mods::interp diff --git a/src/dusk/mods/svc/interp.cpp b/src/dusk/mods/svc/interp.cpp new file mode 100644 index 0000000000..52e4fcdf36 --- /dev/null +++ b/src/dusk/mods/svc/interp.cpp @@ -0,0 +1,92 @@ +#include "mods/svc/interp.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "dusk/interp/menus.h" +#include "internal.hpp" +#include "registry.hpp" + +namespace dusk::mods::svc { + +namespace { + +struct ModDatum { + absl::flat_hash_set registered_matrices; +}; + +absl::flat_hash_map modData; + +static_assert(std::is_same_v); + +bool lookup_replacement_impl(const void* key, InterpMtx out) { + return interp::lookup_replacement(key, out); +} + +ModResult record_final_mtx_keyed(ModContext* context, InterpMtx matrix, void const* key) { + auto* mod = mod_from_context(context); + if (mod == nullptr || !key || !matrix) { + return MOD_INVALID_ARGUMENT; + } + + auto& modDatum = modData[mod]; + modDatum.registered_matrices.insert(key); + + interp::record_final_mtx(matrix, key); + + return MOD_OK; +} + +ModResult record_final_mtx(ModContext* context, InterpMtx matrix) { + return record_final_mtx_keyed(context, matrix, matrix); +} + +ModResult forget_mtx(ModContext* context, void const* key) { + auto* mod = mod_from_context(context); + if (mod == nullptr || !key) { + return MOD_INVALID_ARGUMENT; + } + + auto& modDatum = modData[mod]; + if (!modDatum.registered_matrices.erase(key)) { + // Don't report an error in this case — avoids overcomplicating lifecycle management by + // requiring callers to track if they've ever called record_final_mtx. + return MOD_OK; + } + + interp::forget_mtx(key); + return MOD_OK; +} + +void mod_detached(LoadedMod& mod) { + auto const found = modData.find(&mod); + if (found == modData.end()) { + return; + } + + auto& datum = found->second; + for (auto const key : datum.registered_matrices) { + interp::forget_mtx(key); + } + + modData.erase(found); +} + +constexpr InterpService s_interpService { + .header = SERVICE_HEADER(InterpService, INTERP_SERVICE_MAJOR, INTERP_SERVICE_MINOR), + .record_mtx_keyed = record_final_mtx_keyed, + .record_mtx = record_final_mtx, + .forget_mtx = forget_mtx, + .lookup_replacement_mtx = lookup_replacement_impl, +}; + +} + +ServiceModule const g_interpModule { + .id = INTERP_SERVICE_ID, + .majorVersion = INTERP_SERVICE_MAJOR, + .minorVersion = INTERP_SERVICE_MINOR, + .service = &s_interpService, + .modDetached = mod_detached +}; + +} \ No newline at end of file diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 69903fcf4f..c820602e6a 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -268,6 +268,7 @@ void ModLoader::init_services() { &svc::g_logModule, &svc::g_resourceModule, &svc::g_fileModule, + &svc::g_interpModule, &svc::g_httpModule, &svc::g_netModule, &svc::g_websocketModule, diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 9126f8f55b..8d7f101104 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -76,6 +76,7 @@ extern const ServiceModule g_hostModule; extern const ServiceModule g_logModule; extern const ServiceModule g_resourceModule; extern const ServiceModule g_fileModule; +extern const ServiceModule g_interpModule; extern const ServiceModule g_httpModule; extern const ServiceModule g_netModule; extern const ServiceModule g_websocketModule;