Mods: interpolation service (#2636)

This exposes the basic matrix API to mods through a new service.

I also included a RAII C++ wrapper since, well, convenient.
This commit is contained in:
Pieter-Jan Briers
2026-09-29 22:47:09 +02:00
committed by GitHub
parent 1056b9e8d2
commit 7d8b945e30
6 changed files with 232 additions and 0 deletions
+1
View File
@@ -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
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#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);
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <cstring>
#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
+92
View File
@@ -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<void const*> registered_matrices;
};
absl::flat_hash_map<LoadedMod const*, ModDatum> modData;
static_assert(std::is_same_v<InterpMtx, Mtx>);
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
};
}
+1
View File
@@ -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,
+1
View File
@@ -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;