Skip to content

Instantly share code, notes, and snippets.

@dwilliamson
Last active September 25, 2019 12:53
Show Gist options
  • Save dwilliamson/8fb72ed4be7b5022b856eb4ff2d05d04 to your computer and use it in GitHub Desktop.
Save dwilliamson/8fb72ed4be7b5022b856eb4ff2d05d04 to your computer and use it in GitHub Desktop.
#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;
}
// 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;
};
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