In C++ I have the following template:
template<typename UBO>
std::pair<uint, std::vector<std::byte>> SerializeUniform(UBO& ubo, uint binding)
{
// Create raw binary buffers of the uniform data
std::vector<std::byte> ubo_buffer(sizeof(UBO));
memcpy(ubo_buffer.data(), (void*)&ubo, sizeof(UBO));
return {binding, ubo_buffer};
}
inline void SerializeArguments(NECore::UniformBufferDataContainer& data) {}
template<typename T1, typename... Ts>
inline void SerializeArguments(
NECore::UniformBufferDataContainer& data, T1& uniform, uint binding, Ts&... args)
{
data.push_back(SerializeUniform(uniform, binding));
SerializeArguments(data, args...);
}
template<class... Ubos>
void ModuleStorage::Draw(const NECore::RenderRequest& render_request, const Ubos&... args)
{
size_t arg_num = _details::ArgNum(args...);
NECore::UniformBufferDataContainer ubos;
ubos.reserve(arg_num);
_details::SerializeArguments(ubos, args...);
if(render_request.image_outputs.empty())
DrawToScreen(vk_meta_data.vulkan_data, render_request, ubos);
else
DrawOffScreen(vk_meta_data.vulkan_data, render_request, ubos);
}
A bit hard to read so let me walk you through them.
The first template takes a POD and copies it's data into a vector of bytes and then creates a tuple to associate the data with an integer.
The next template is the base case of a recursive template, no parameters, do nothing.
Next we have a recursive template, take the first two parameters, assumed to be a POD and an integer, And serialize the POD. Recurse on the tail.
I.e. this template allows me to serialize an arbitrary number of PODs.
Finally I have a variadic template that allows me to serialize any number of PODs.
You might be wondering why go through all this trouble. It;s so that I can write things like this:
modules.Draw(
{
gltf_shader,
{gallery.GetGpuMeshData(model_names[selected_model])},
textures,
ssbos
},
mvp, 0,
gltf_info, 1);
This way the render command accepts any arbitrary number of uniform parameters which means I can use the same pattern and syntax to call any arbitrary shader my heart desires with any inputs I want (as long as they are byte compatible with the shader declaration)
I am porting this library to rust and I want to achieve a similar thing with macros. i.e. I want to be abble to define things such that I can call
draw(render_request, macro!(ubo1, 0, ubo2, 1))
Or better yet (but I am almost certain this cannot be done in rust)
draw(render_request, ubo1, 0, ubo2, 1)
I am having a very hard time trying to come up with the macro. The primary issue is, macros are not functions and rust doesn't support variadic arguments. I am not entirely sure how to define the macro to achieve what I want.
I managed to get it to work by hacking the vec!
macro from the standard library
macro_rules! UBO {
() => {Vec::<UniformBufferData>::new()};
($($ubo:expr, $binding : expr),* $(,)?) =>
{
[$(serialize_uniform(&$ubo, $binding)),+].to_vec()
}
}
This lets you call the macro UBO!(dummy2, 0, dummy, 1, dummy3, 3)
Which expands into
[
serialize_uniform(&dummy2, 0),
serialize_uniform(&dummy, 1),
serialize_uniform(&dummy3, 3),
]
.to_vec()
So now you can do:
draw(render_request, UBO!(var1, 0, var2, 2, var3, 7));
And it should work (UB and other shennanigans aside).