Refactoring math library

Original link: https://blog.codingnow.com/2022/07/refactor_math3d.html

The math library used in our engine has been tinkered with for a long time. During this period, many ideas were accumulated. Recently, I am doing performance optimization of the engine, rewriting some hotspot systems found in C/C++, and refactoring the math library by the way, so that it can better take into account the Lua API and the C API.

The last time the math library was improved was three years ago . After three years of use, we found that although a stack-based DSL can reduce the communication cost between Lua and C, it is not convenient to use. Most of the time, we still prefer a more traditional interface: one function call per math operation. And high-complexity mathematical operations can be completed in a separate C module. Combined with the ECS system, we can batch the same but huge number of mathematical operations in the C side set.

We abandoned the original design DSL a long time ago and only used parts of the math library. Taking advantage of this refactoring, I plan to completely delete these abandoned parts and redesign the underlying data structure to better adapt to its core features.

I think the core feature of our math library is that all math objects, including matrices, vectors, and quaternions, are immutable value objects with a consistent appearance. I express it as a 64bit id, which is convenient for Lua (or other languages) binding.

When doing Lua binding, the math object id is a lightuserdata instead of the heavyweight userdata used by traditional libraries. Compared with ordinary numbers, the cost used in this way has no obvious additional burden.

Even if not used in Lua, when used directly by C/C++, a 64bit id is better than a larger (and unequally sized) data structure or smart pointer equivalent. When used in C/C++, it is also possible to treat blocks of data such as matrices as lightweight values ​​like integers.

To do this, the most difficult thing is object lifetime management. I’m assuming that in use, most of the math objects are disposable, a complex process, only the last link is passed into other modules. And other third-party modules (such as physics, animation, rendering) usually have their own solutions, and do not rely on the caller to maintain the lifetime of mathematical objects (matrix, etc.).

Therefore, our math library allocates storage space for math objects on a fixed temporary memory block by default. In the new implementation, this temporary memory is up to 256 pages, each with 1024 float4 spaces, and can store up to 64K matrices or 256K vector4. Temporary space is cleaned up between rendering frames; within the same frame, objects are not deleted and are always valid.

Allocating temporary objects uses the simplest bump allocator. Creating a new object id only requires one addition, so it is almost as efficient as stack allocation, so we no longer need to consider whether to put math objects on the stack or the heap.

For objects that need persistent references, the data in the temporary object area can be copied to a permanent object area. Unlike previous implementations, objects in the persistent object area use partial reference counting, which reduces the cost of multiple references.

The corresponding api is mark and unmark, which can be understood as, if you want to refer to an id permanently, then call mark(id) to generate a new permanent id. Remember, when you don’t need to use it, call unmark(id) to give up the reference. When passing a reference, if the receiver also wants to refer to it, it also needs to be marked again; and the implementation can choose to increase the reference internally, or generate an additional copy.

Because we treat all ids as immutable mathematical objects, it is equivalent for the user to choose whether to refer to the same piece of memory or copy the data to get a new copy.

unmark should not be regarded as delete or release, because it does not reclaim memory immediately, so after the id is unmarked, it can continue to be used until the end of the frame. In this way, for application transfer, there is no need to call mark deliberately when passing a reference to ensure the validity of the id. (This is different from the traditional smart pointer reference scheme)

In this refactoring, I added two important new features. This is a very necessary function summed up in our several years of experience.

First, the NULL type has been added. Where a mathematical object should appear in the interface, a NULL object can be passed to distinguish it from normal matrices and other objects.

This makes interface design more flexible. In permanently referenced data structures, NULL can also be used as the default value to facilitate certain optimizations. For example, for the interface of synthesizing SRT to a matrix, it is allowed to pass any parameter of S/R/T as NULL to reduce the amount of calculation.

Second, increase the array type. To be precise, any data object is an array, but the default array length is 1. An array of matrices can be treated as a single matrix (the first element of the array); you can also use the index (light) api to retrieve a feature element in the array.

With this feature, objects like AABB (two vectors), view frustum (six vectors), etc. can be expressed more conveniently; C API design can also return multiple calculation results more conveniently (just return an array) .


It took me three days to complete the refactoring work, and the current version can be seen at https://github.com/cloudwu/math3d . The Lua library is the part we mainly use, but the mathid submodule can be called directly by C/C++.

This article is reprinted from: https://blog.codingnow.com/2022/07/refactor_math3d.html
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment