Last active
September 25, 2019 12:53
-
-
Save dwilliamson/8fb72ed4be7b5022b856eb4ff2d05d04 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "FunctionalPropertyModifiers.h" | |
void fpmCallStore::CallAll(double time) | |
{ | |
for (auto i : m_FunctionCallMap) | |
{ | |
const fpmFunctionCalls& calls = i->value; | |
calls.call_all_fn(i->key, calls, time); | |
} | |
} | |
void fpmCallStore::AddCallP(intptr_t function_key, fpmFunctionCalls::CallType make_all_calls, double t0, | |
double t1, void* call_data, uint32_t call_data_size) | |
{ | |
// If the target function isn't already mapped in the hash table, add an empty list of calls for it | |
auto i = m_FunctionCallMap.find(function_key); | |
if (i == nullptr) | |
{ | |
fpmFunctionCalls calls(call_data_size); | |
calls.call_all_fn = make_all_calls; | |
i = m_FunctionCallMap.insert(function_key, calls); | |
} | |
fpmFunctionCalls& calls = i->value; | |
// Grow the type-erased call data buffer on-demand | |
if (calls.call_data_buffer_pos + call_data_size > calls.call_data_buffer.size) | |
{ | |
uint32_t new_size = uint32_t((calls.call_data_buffer.size + call_data_size) * 1.5); | |
calls.call_data_buffer.Resize(new_size); | |
} | |
// Add this new call to the end of the list | |
memcpy(calls.call_data_buffer.data + calls.call_data_buffer_pos, &call_data, call_data_size); | |
calls.call_data_buffer_pos += call_data_size; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Function Property Modifiers | |
// --------------------------- | |
// | |
// Very fast means of functionally updating raw math properties over time without requiring | |
// custom update loops per object. The key observation is that there will be *many* varying function | |
// inputs for the same function in a frame. For example, lots of properties will want to call `lerp` | |
// on each update. | |
// | |
// Performance goals are achieved by: | |
// | |
// * All function calls are inlined with no per-value indirect branching. | |
// * Constant looping keeps the code cache warm. | |
// * All inputs are stored in contiguous memory. | |
// * Input and output data can be very easily speculatively prefetched by the CPU. | |
// * No proxy objects in the target slowing down access each time the values are fetched. | |
// | |
// Debug performance has also been optimised for. Profiling the effect of using this library is | |
// localised and trivial to measure/budget, unlike libraries that add sampling proxy objects to | |
// their targets. | |
// | |
// Given a property it can be modified over time with: | |
// | |
// fpmCallStore store; | |
// float x; | |
// fpmApply(x, store, lerp, 1, 10, 0.5f, 0.8f); | |
// | |
// This will linearly interpolate `x` from 0.5 to 0.8 between the time 1 and 10. | |
// | |
#pragma once | |
#if defined(__clcpp_parse__) || defined(MATH_HLSL) | |
#define MATH_API | |
#else | |
#ifdef MATH_IMPL | |
#define MATH_API __declspec(dllexport) | |
#else | |
#define MATH_API __declspec(dllimport) | |
#endif | |
#endif | |
#include <TinyCRT/TinyCRT.h> | |
#include <Memory/Memory.h> | |
#include <Core/Containers.h> | |
// ----------------------------------------------------------------------------------------- | |
// Public API | |
// ----------------------------------------------------------------------------------------- | |
// Stores and calls all function calls in a frame. | |
class fpmCallStore; | |
// Setup a single value property for functional modification over time. | |
// | |
// `target` Target value. | |
// `store` Call store for allocation and later calling. | |
// `function` Function to call for this set of inputs. | |
// `t0` Start of the time range for this function. | |
// `t1` End of the time range for this function. | |
// `arguments...` Variable list of arguments to pass to `function` on each call. | |
// | |
// When the function is called using `fpmCallStore::CallAll` the input global time will be mapped | |
// to [0,1] using this call's time range before being passed to `function` as the last parameter. | |
// The global time is clamped to the input range before the call. | |
template <typename TargetType, typename Function, typename... Arguments> | |
void fpmApply(TargetType& target, fpmCallStore& store, Function function, double t0, double t1, Arguments&&... arguments) | |
{ | |
store.AddCall(target, function, t0, t1, arguments...); | |
} | |
// Setup a vector value properties for functional modification over time. | |
// | |
// `target` Target vector. | |
// `store` Call store for allocation and later calling. | |
// `function` Function to call for this set of inputs. | |
// `t0` Start of the time range for this function. | |
// `t1` End of the time range for this function. | |
// `arguments...` Variable list of arguments to pass to `function` on each call. | |
// | |
// When the function is called using `fpmCallStore::CallAll` the input global time will be mapped | |
// to [0,1] using this call's time range before being passed to `function` as the last parameter. | |
// The global time is clamped to the input range before the call. | |
template <typename TargetType, typename Function, typename... Arguments> | |
void fpmApply2(TargetType& target, fpmCallStore& store, Function function, double t0, double t1, Arguments&&... arguments) | |
{ | |
store.AddCall(target.x, function, t0, t1, (arguments.x)...); | |
store.AddCall(target.y, function, t0, t1, (arguments.y)...); | |
} | |
template <typename TargetType, typename Function, typename... Arguments> | |
void fpmApply3(TargetType& target, fpmCallStore& store, Function function, double t0, double t1, Arguments&&... arguments) | |
{ | |
store.AddCall(target.x, function, t0, t1, (arguments.x)...); | |
store.AddCall(target.y, function, t0, t1, (arguments.y)...); | |
store.AddCall(target.z, function, t0, t1, (arguments.z)...); | |
} | |
template <typename TargetType, typename Function, typename... Arguments> | |
void fpmApply4(TargetType& target, fpmCallStore& store, Function function, double t0, double t1, Arguments&&... arguments) | |
{ | |
store.AddCall(target.x, function, t0, t1, (arguments.x)...); | |
store.AddCall(target.y, function, t0, t1, (arguments.y)...); | |
store.AddCall(target.z, function, t0, t1, (arguments.z)...); | |
store.AddCall(target.w, function, t0, t1, (arguments.w)...); | |
} | |
// ----------------------------------------------------------------------------------------- | |
// Implementation Dependencies | |
// ----------------------------------------------------------------------------------------- | |
// Pack an arbitrary number of arguments next to each other in a data structure. This is effectively | |
// a replacement for std::tuple, sacrificing a concise implementation for faster debug performance: | |
// | |
// * No repeated calls to our equivalent of std::get/std::forward. | |
// * No external call to convert bound arguments to a variadic argument list. | |
// | |
// ...a cleaner in-memory view of the arguments: | |
// | |
// * All arguments are next to each other in the same type instead of being repeatedly inherited. | |
// | |
// ...and faster compiles with easier to understand code: | |
// | |
// * No use of our std::make_index_sequence and std::index_sequence equivalents. | |
// | |
// As this implementation is specific to Functional Property Modifiers, they also have a `Call` | |
// function with expected return type, for a smaller code footprint. This expects a final parameter | |
// within [0,1] used to specify the time. | |
template <typename Ret, typename ...> | |
struct fpmArgumentPack | |
{ | |
template <typename Function> Ret Call(Function function, float t) const | |
{ | |
return function(t); | |
} | |
}; | |
template <typename Ret, typename A> | |
struct fpmArgumentPack<Ret, A> | |
{ | |
A a; | |
template <typename Function> Ret Call(Function function, float t) const | |
{ | |
return function(a, t); | |
} | |
}; | |
template <typename Ret, typename A, typename B> | |
struct fpmArgumentPack<Ret, A, B> | |
{ | |
A a; B b; | |
template <typename Function> Ret Call(Function function, float t) const | |
{ | |
return function(a, b, t); | |
} | |
}; | |
template <typename Ret, typename A, typename B, typename C> | |
struct fpmArgumentPack<Ret, A, B, C> | |
{ | |
A a; B b; C c;; | |
template <typename Function> Ret Call(Function function, float t) const | |
{ | |
return function(a, b, c, t); | |
} | |
}; | |
template <typename Ret, typename A, typename B, typename C, typename D> | |
struct fpmArgumentPack<Ret, A, B, C, D> | |
{ | |
A a; B b; C c; D d; | |
template <typename Function> Ret Call(Function function, float t) const | |
{ | |
return function(a, b, c, d, t); | |
} | |
}; | |
template <typename Ret, typename A, typename B, typename C, typename D, typename E> | |
struct fpmArgumentPack<Ret, A, B, C, D, E> | |
{ | |
A a; B b; C c; D d; E e; | |
template <typename Function> Ret Call(Function function, float t) const | |
{ | |
return function(a, b, c, d, e, t); | |
} | |
}; | |
// Records the time range a function should be applied over and the target | |
// destination of the function result. | |
template <typename TargetType> | |
struct fpmTimeData | |
{ | |
fpmTimeData(TargetType* target, double t0, double t1) | |
: t0(t0) | |
, ts(1.0 / (t1 - t0)) | |
, target(target) | |
{ | |
} | |
// Start time | |
double t0; | |
// Stores `1/(t1-t0)` because it's just a little quicker when vectors are | |
// added as individual floats and `t` is recalculated. | |
double ts; | |
// Pointer to the function result destination | |
TargetType* target; | |
}; | |
// All data required for a single call to a property function. | |
template <typename ResultType, typename... Arguments> | |
struct fpmCallData | |
{ | |
fpmTimeData<ResultType> time_data; | |
fpmArgumentPack<ResultType, Arguments...> arguments; | |
}; | |
// Maps a single function to a list of many inputs, to be called in a tight loop. | |
struct fpmFunctionCalls | |
{ | |
fpmFunctionCalls(uint32_t call_data_size) | |
: call_data_buffer(call_data_size) | |
, call_data_buffer_pos(0) | |
{ | |
} | |
// Type-erased call data list | |
memDynamicBuffer call_data_buffer; | |
uint32_t call_data_buffer_pos; | |
// Function that will iterate all call data objects and call the same | |
// time-based function for them. | |
using CallType = void (*)(intptr_t, const fpmFunctionCalls&, double); | |
CallType call_all_fn; | |
}; | |
class fpmCallStore | |
{ | |
public: | |
template <typename TargetType, typename Function, typename... Arguments> | |
void AddCall(TargetType& target, Function function, double t0, double t1, Arguments&&... arguments) | |
{ | |
// Pack time data and arguments into a single object | |
using CallData = fpmCallData<TargetType, core::Decay<Arguments>...>; | |
fpmTimeData<TargetType> time_data(&target, t0, t1); | |
CallData call_data{time_data, {arguments...}}; | |
intptr_t function_key = reinterpret_cast<intptr_t>(function); | |
AddCallP(function_key, &MakeAllCalls<decltype(function), CallData>, t0, t1, &call_data, sizeof(call_data)); | |
} | |
MATH_API void CallAll(double time); | |
private: | |
MATH_API void AddCallP(intptr_t function_key, fpmFunctionCalls::CallType make_all_calls, double t0, | |
double t1, void* call_data, uint32_t call_data_size); | |
template <typename FunctionType, typename CallData> | |
static void MakeAllCalls(intptr_t function_key, const fpmFunctionCalls& calls, double time) | |
{ | |
FunctionType function = reinterpret_cast<FunctionType>(function_key); | |
// Iterate all call inputs | |
CallData* call_datas = (CallData*)calls.call_data_buffer.data; | |
CallData* call_datas_end = call_datas + calls.call_data_buffer_pos / sizeof(CallData); | |
while (call_datas < call_datas_end) | |
{ | |
const CallData& call_data = *call_datas++; | |
// Map input time to unit range | |
const auto& time_data = call_data.time_data; | |
float t = (float)saturate((time - time_data.t0) * time_data.ts); | |
// Call the target function | |
*time_data.target = call_data.arguments.Call(function, t); | |
} | |
} | |
// Map from function address to list of required calls and their inputs | |
core::ProbeHashTable<intptr_t, fpmFunctionCalls> m_FunctionCallMap; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct Object | |
{ | |
float a, b, c; | |
float s; | |
float3 v; | |
float3 col; | |
}; | |
float3 Geoffrey(float t) | |
{ | |
float3 r = t * 2.1f - float3(1.8f, 1.14f, 0.3f); | |
return 1.0f - r * r; | |
} | |
float lerp(float a, float b, float t) | |
{ | |
return a + t * (b - a); | |
} | |
float spline(float a, float b, float c, float d, float t) | |
{ | |
float t2 = t * t; | |
float t3 = t2 * t; | |
return float(0.5) * (2 * b + (c - a) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (d - a + 3 * b - 3 * c) * t3); | |
} | |
void test() | |
{ | |
fpmCallStore s; | |
Object o; | |
// Modify a single value | |
fpmApply(o.a, s, lerp, 1.0, 10.0, 53.0f, -8.0f); | |
fpmApply(o.b, s, lerp, 5.0f, 7.0f, 101.0f, 2058.0f); | |
fpmApply(o.c, s, lerp, 2.0f, 3.0f, 0.0f, 1.0f); | |
fpmApply(o.s, s, spline, 1.1, 8.8, 1.0f, 2.0f, 3.0f, 4.0f); | |
// Modify a vector value | |
fpmApply(o.col, s, Geoffrey, 2, 20); | |
// Modify a vector value by applying the same function to all vector components | |
fpmApply3(o.v, s, lerp, 0.2, 7.5, float3(1,3,4), float3(10,44,123)); | |
double time = 2.7; | |
s.CallAll(time); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment