Optimizing entityposition as much as possible.

This commit is contained in:
2026-05-21 13:17:48 -05:00
parent cdf5a5229c
commit 23e617ea21
2 changed files with 129 additions and 63 deletions
@@ -14,32 +14,68 @@ static void entityPositionEnsurePRS(entityposition_t *pos) {
pos->flags &= ~ENTITY_POSITION_FLAG_PRS_DIRTY;
}
// Rebuild localTransform from the PRS cache. Only called when LOCAL_DIRTY.
// Rebuild localTransform from the PRS cache. Only rebuilds what changed.
static void entityPositionEnsureLocal(entityposition_t *pos) {
if(!(pos->flags & ENTITY_POSITION_FLAG_LOCAL_DIRTY)) return;
glm_mat4_identity(pos->localTransform);
glm_translate(pos->localTransform, pos->position);
glm_rotate_x(pos->localTransform, pos->rotation[0], pos->localTransform);
glm_rotate_y(pos->localTransform, pos->rotation[1], pos->localTransform);
glm_rotate_z(pos->localTransform, pos->rotation[2], pos->localTransform);
glm_scale(pos->localTransform, pos->scale);
pos->flags &= ~ENTITY_POSITION_FLAG_LOCAL_DIRTY;
const uint8_t dirty = pos->flags & (
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
);
if(!dirty) return;
if(dirty & ENTITY_POSITION_FLAG_ROTATION_DIRTY) {
// Rotation or scale changed: rebuild columns 0-2 analytically (XYZ euler order).
const float c0 = cosf(pos->rotation[0]), s0 = sinf(pos->rotation[0]);
const float c1 = cosf(pos->rotation[1]), s1 = sinf(pos->rotation[1]);
const float c2 = cosf(pos->rotation[2]), s2 = sinf(pos->rotation[2]);
const float s0s1 = s0 * s1;
const float c0s1 = c0 * s1;
pos->localTransform[0][0] = c1 * c2 * pos->scale[0];
pos->localTransform[0][1] = (c0 * s2 + s0s1 * c2) * pos->scale[0];
pos->localTransform[0][2] = (s0 * s2 - c0s1 * c2) * pos->scale[0];
pos->localTransform[0][3] = 0.0f;
pos->localTransform[1][0] = -c1 * s2 * pos->scale[1];
pos->localTransform[1][1] = (c0 * c2 - s0s1 * s2) * pos->scale[1];
pos->localTransform[1][2] = (s0 * c2 + c0s1 * s2) * pos->scale[1];
pos->localTransform[1][3] = 0.0f;
pos->localTransform[2][0] = s1 * pos->scale[2];
pos->localTransform[2][1] = -s0 * c1 * pos->scale[2];
pos->localTransform[2][2] = c0 * c1 * pos->scale[2];
pos->localTransform[2][3] = 0.0f;
}
if(dirty & ENTITY_POSITION_FLAG_POSITION_DIRTY) {
// Only position changed: update column 3 only (no trig needed).
pos->localTransform[3][0] = pos->position[0];
pos->localTransform[3][1] = pos->position[1];
pos->localTransform[3][2] = pos->position[2];
pos->localTransform[3][3] = 1.0f;
}
pos->flags &= ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY);
}
// Recompute worldTransform from the parent chain. Only called when WORLD_DIRTY.
static void entityPositionEnsureWorld(entityposition_t *pos) {
entityPositionEnsureLocal(pos);
if(!(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY)) return;
entityPositionEnsureLocal(pos);
if(pos->parentEntityId == ENTITY_ID_INVALID) {
glm_mat4_copy(pos->localTransform, pos->worldTransform);
} else {
if(pos->parentEntityId != ENTITY_ID_INVALID) {
// Parented: world = parent.world × local. worldTransform must be written
// because children (and this node's getters) read it.
entityposition_t *parent = componentGetData(
pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(parent);
glm_mat4_mul(parent->worldTransform, pos->localTransform, pos->worldTransform);
} else if(pos->childCount > 0) {
// Parentless root with children: children need a valid worldTransform to
// multiply against, but world == local, so just copy.
glm_mat4_copy(pos->localTransform, pos->worldTransform);
}
// Parentless leaf: world == local. Getters read localTransform directly;
// no copy needed.
pos->flags &= ~ENTITY_POSITION_FLAG_WORLD_DIRTY;
}
@@ -63,15 +99,15 @@ void entityPositionInit(
entityId, componentId, COMPONENT_TYPE_POSITION
);
pos->flags = 0;
pos->parentEntityId = ENTITY_ID_INVALID;
pos->parentComponentId = COMPONENT_ID_INVALID;
pos->childCount = 0;
glm_vec3_zero(pos->position);
glm_vec3_zero(pos->rotation);
glm_vec3_one(pos->scale);
glm_mat4_identity(pos->localTransform);
glm_mat4_identity(pos->worldTransform);
pos->flags = 0;
pos->parentEntityId = ENTITY_ID_INVALID;
pos->parentComponentId = COMPONENT_ID_INVALID;
pos->childCount = 0;
}
void entityPositionLookAt(
@@ -87,7 +123,7 @@ void entityPositionLookAt(
glm_lookat(eye, target, up, pos->localTransform);
// localTransform is now authoritative; PRS cache is stale.
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_PRS_DIRTY)
& ~ENTITY_POSITION_FLAG_LOCAL_DIRTY;
& ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY);
entityPositionMarkDirty(pos);
}
@@ -100,7 +136,10 @@ void entityPositionGetTransform(
entityId, componentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(pos);
glm_mat4_copy(pos->worldTransform, dest);
glm_mat4_copy(
pos->parentEntityId == ENTITY_ID_INVALID ? pos->localTransform : pos->worldTransform,
dest
);
}
void entityPositionGetLocalTransform(
@@ -135,6 +174,12 @@ void entityPositionGetPosition(
entityposition_t *pos = componentGetData(
entityId, componentId, COMPONENT_TYPE_POSITION
);
// Parentless fast path: world position == pos->position, no matrix needed.
if(pos->parentEntityId == ENTITY_ID_INVALID) {
entityPositionEnsurePRS(pos);
glm_vec3_copy(pos->position, dest);
return;
}
entityPositionEnsureWorld(pos);
dest[0] = pos->worldTransform[3][0];
dest[1] = pos->worldTransform[3][1];
@@ -150,7 +195,7 @@ void entityPositionSetPosition(
entityId, componentId, COMPONENT_TYPE_POSITION
);
glm_vec3_copy(position, pos->position);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_LOCAL_DIRTY)
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos);
}
@@ -176,7 +221,7 @@ void entityPositionSetRotation(
entityId, componentId, COMPONENT_TYPE_POSITION
);
glm_vec3_copy(rotation, pos->rotation);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_LOCAL_DIRTY)
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos);
}
@@ -202,7 +247,7 @@ void entityPositionSetScale(
entityId, componentId, COMPONENT_TYPE_POSITION
);
glm_vec3_copy(scale, pos->scale);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_LOCAL_DIRTY)
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos);
}
@@ -265,7 +310,7 @@ entityposition_t *entityPositionGet(
}
void entityPositionRebuild(entityposition_t *pos) {
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_LOCAL_DIRTY)
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos);
}
@@ -324,36 +369,33 @@ void entityPositionDecompose(entityposition_t *pos) {
pos->localTransform[2][2] * pos->localTransform[2][2]
);
// Normalize columns to isolate the rotation matrix.
float invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
float invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
float invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
// Normalize columns to isolate the rotation matrix (9 floats, no mat4 needed).
const float invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
const float invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
const float invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
mat4 r;
glm_mat4_identity(r);
r[0][0] = pos->localTransform[0][0] * invS0;
r[0][1] = pos->localTransform[0][1] * invS0;
r[0][2] = pos->localTransform[0][2] * invS0;
r[1][0] = pos->localTransform[1][0] * invS1;
r[1][1] = pos->localTransform[1][1] * invS1;
r[1][2] = pos->localTransform[1][2] * invS1;
r[2][0] = pos->localTransform[2][0] * invS2;
r[2][1] = pos->localTransform[2][1] * invS2;
r[2][2] = pos->localTransform[2][2] * invS2;
const float r00 = pos->localTransform[0][0] * invS0;
const float r01 = pos->localTransform[0][1] * invS0;
const float r02 = pos->localTransform[0][2] * invS0;
const float r10 = pos->localTransform[1][0] * invS1;
const float r11 = pos->localTransform[1][1] * invS1;
const float r20 = pos->localTransform[2][0] * invS2;
const float r21 = pos->localTransform[2][1] * invS2;
const float r22 = pos->localTransform[2][2] * invS2;
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
float sinBeta = glm_clamp(r[2][0], -1.0f, 1.0f);
const float sinBeta = glm_clamp(r20, -1.0f, 1.0f);
pos->rotation[1] = asinf(sinBeta);
float cosBeta = cosf(pos->rotation[1]);
const float cosBeta = cosf(pos->rotation[1]);
if(fabsf(cosBeta) > 1e-6f) {
pos->rotation[0] = atan2f(-r[2][1], r[2][2]);
pos->rotation[2] = atan2f(-r[1][0], r[0][0]);
pos->rotation[0] = atan2f(-r21, r22);
pos->rotation[2] = atan2f(-r10, r00);
} else {
// Gimbal lock: pin Z to 0, recover X.
pos->rotation[2] = 0.0f;
pos->rotation[0] = (sinBeta > 0.0f)
? atan2f(r[0][1], r[1][1])
: -atan2f(r[0][1], r[1][1]);
? atan2f(r01, r11)
: -atan2f(r01, r11);
}
}
@@ -15,31 +15,33 @@
* PRS cache is stale. localTransform was written directly (e.g. lookAt) and
* position/rotation/scale need to be decomposed before they can be read.
*/
#define ENTITY_POSITION_FLAG_PRS_DIRTY (1 << 0)
#define ENTITY_POSITION_FLAG_PRS_DIRTY (1 << 0)
/**
* localTransform is stale. PRS was updated via a setter; the matrix needs to
* be rebuilt from position/rotation/scale before it can be used.
* Columns 0-2 of localTransform are stale. Rotation or scale changed; the
* basis vectors need to be rebuilt analytically before the matrix can be used.
* Does not imply column 3 (translation) is stale.
*/
#define ENTITY_POSITION_FLAG_LOCAL_DIRTY (1 << 1)
#define ENTITY_POSITION_FLAG_ROTATION_DIRTY (1 << 1)
/**
* Column 3 of localTransform is stale. Position changed; only the translation
* column needs to be written. Does not imply columns 0-2 are stale.
*/
#define ENTITY_POSITION_FLAG_POSITION_DIRTY (1 << 2)
/**
* worldTransform is stale. Either the local matrix changed or an ancestor
* moved; the full parent-chain multiply must be rerun before world data is read.
*/
#define ENTITY_POSITION_FLAG_WORLD_DIRTY (1 << 2)
#define ENTITY_POSITION_FLAG_WORLD_DIRTY (1 << 3)
typedef struct {
/** Local transform matrix, rebuilt lazily from position/rotation/scale. */
mat4 localTransform;
/** World transform matrix, recomputed lazily from the parent chain. */
mat4 worldTransform;
/** Cached local position (XYZ). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
vec3 position;
/** Cached local euler rotation (XYZ, radians). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
vec3 rotation;
/** Cached local scale (XYZ). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
vec3 scale;
/*
* Hot fields — flag checks and parent/child traversal (markDirty, ensureWorld)
* only touch these. Kept at the front so they share the first cache line.
*/
/** Bitmask of ENTITY_POSITION_FLAG_* values describing which caches are stale. */
uint8_t flags;
/** Entity ID of the parent node, or ENTITY_ID_INVALID if none. */
@@ -52,6 +54,27 @@ typedef struct {
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
/** Component IDs of child position components. */
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
/*
* Warm fields — read/written by PRS getters/setters.
* Accessed more often than the matrices but less often than flags.
*/
/** Cached local position (XYZ). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
vec3 position;
/** Cached local euler rotation (XYZ, radians). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
vec3 rotation;
/** Cached local scale (XYZ). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
vec3 scale;
/*
* Cold fields — only touched when actually rebuilding transforms.
*/
/** Local transform matrix, rebuilt lazily from position/rotation/scale. */
mat4 localTransform;
/** World transform matrix, recomputed lazily from the parent chain. */
mat4 worldTransform;
} entityposition_t;
/**
@@ -241,10 +264,11 @@ entityposition_t *entityPositionGet(
);
/**
* Signals that the PRS cache was modified externally. Sets
* ENTITY_POSITION_FLAG_LOCAL_DIRTY so localTransform is rebuilt lazily on the
* next read, clears ENTITY_POSITION_FLAG_PRS_DIRTY, and propagates
* ENTITY_POSITION_FLAG_WORLD_DIRTY to self and all descendants.
* Signals that the PRS cache was modified externally. Sets both
* ENTITY_POSITION_FLAG_ROTATION_DIRTY and ENTITY_POSITION_FLAG_POSITION_DIRTY
* so all of localTransform is rebuilt lazily on the next read, clears
* ENTITY_POSITION_FLAG_PRS_DIRTY, and propagates ENTITY_POSITION_FLAG_WORLD_DIRTY
* to self and all descendants.
*
* @param pos The position component whose PRS was modified.
*/