Storage
Several implementations of the matrix types are available, some of
which store the elements of matrices data in direct memory
. This allows for zero-copy passing of the matrix data
to native code
. Use of direct memory does incur an extra cost with regards
to creation and deletion of matrices, and some functions in the package do
require temporary matrices in order to work. Therefore, the package provides
Context types that allow the user to pre-allocate
storage that the functions can reuse an unlimited number of times. The package
typically defines one
Context type per class.
See the
MatrixM4x4D.ContextMM4D
type for a concrete example; any function in the
MatrixM4x4D
class that takes a
ContextMM4D as an argument can be
depended upon not to allocate more memory. Note that the
ContextMM4D
type cannot be shared across threads and is completely opaque outside of the
jtensors package: Users should
allocate one
ContextMM4D
per thread.
Matrix data is stored in column-major format
, in whatever is the
platform's native byte order. For an m x m
square matrix, assuming that each element of the matrix uses
n bytes, the first byte of the element at
row r and column c
(assuming 0 <= r < m and
0 <= c < m)
can be found by (c * m * n) + (r * n).
As an example, a 4x4 matrix with 4
byte elements would be stored in memory as shown in the following
diagram:
So, the element at row 0,
column 0 would be stored in
bytes [0 .. 3]. The
element at row 1, column
0 would be stored in bytes
[4 .. 7]. The
element at row 0, column
1 would be stored in bytes
[16 .. 19], and so on.
The immutable matrix types available in the package are not
backed by direct memory and only offer a small fraction of
the functionality of the mutable types. They are essentially
provided as a way to construct immutable snapshots of a mutable
matrix in order to, for example, safely transfer matrix values
across threads without sharing a reference to mutable memory.
Access control
If a function only needs to read from a value of a matrix type, then
it would be desirable to be able to statically enforce this fact. Therefore,
the package provides a set of Readable
interfaces that all matrix types implement. By writing a function that
takes a value of a type implementing a Readable
interface, the programmer is implicitly stating that this function does
not modify the matrix.
If a function needs to write to a value of a matrix type, then
it would be desirable to be able to statically indicate this fact. Therefore,
the package provides a set of Writable
interfaces that all mutable matrix types implement. By writing a function that
takes a value of a type implementing a Writable
interface, the programmer is implicitly stating that this function may
modify the matrix.
A combination of the above interfaces is provided that
essentially abstracts over mutable matrix types. This allows the
programmer to write code against pure interfaces without referring
to a specific type, and to freely substitute alternative
implementations at any time.
Finally, if matrices that are backed by direct memory are required
(in order to pass them directly to native code, for example), the
programmer can require matrices of a
Direct type.
It is also useful for the sizes of matrices to be indicated
in the types, so the package provides a set of interfaces
combining the Readable and
Direct types.
Interfaces that abstract over direct, mutable matrices are also
provided:
Phantom Types
As with the
vector
types, the
io7m-jtensors package
provides copies of all of the existing matrix types (and interfaces) indexed
by a pair of
phantom type parameters.
Conceptually, a matrix can be considered as storing a transform from
coordinate space T0 to space
T1. For a 4x4 mutable matrix
in the io7m-jtensors package,
this is denoted by the type PMatrixM4x4F<T0,T1>.
It then follows that when matrices are concatenated via multiplications,
their type parameters are translated accordingly. For example,
a matrix PMatrixM4x4F<T0,T1>
multiplied by a matrix
PMatrixM4x4F<T1,T2> results
in a matrix of type
PMatrixM4x4F<T0,T2>. Inverting
a matrix results in a matrix that represents the inverse of the original
transform that the matrix represented. For example, inverting a matrix of
type PMatrixM4x4F<T0,T1> results
in a matrix of type
PMatrixM4x4F<T1,T0>.
Type parameters are also translated across multiplications by
vectors. A multiplication of a vector of type
PVectorI4F<T0> by a matrix of
type
PMatrixM4x4F<T0,T1> results in
a vector of type
PVectorI4F<T1>.
Being able to track the types of transforms at this level of detail
is invaluable when using systems such as OpenGL, where accidentally
mixing up matrices tends to result in visual anomalies that can be
extremely hard to track down. By explicitly denoting coordinate spaces
with empty types, it's possible to statically prevent all bugs involving
accidentally mixing up matrices. It's also possible to prevent the
incorrect construction of matrices
. Additionally,
with each matrix labelled by the type of transform it represents, code becomes
self-documenting.
Of course, because the type system is unsound, and because the matrices
are mutable, it is obviously possible to deliberately construct matrices
that claim to be of a particular type of transform but nevertheless
contain nonsense values. It is, however, difficult to do this accidentally.
The io7m-jtensors package strives
to provide a means to prevent accidental errors, and doesn't attempt to
address deliberate sabotage.