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; 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) { static void entityPositionEnsureLocal(entityposition_t *pos) {
if(!(pos->flags & ENTITY_POSITION_FLAG_LOCAL_DIRTY)) return; const uint8_t dirty = pos->flags & (
glm_mat4_identity(pos->localTransform); ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
glm_translate(pos->localTransform, pos->position); );
glm_rotate_x(pos->localTransform, pos->rotation[0], pos->localTransform); if(!dirty) return;
glm_rotate_y(pos->localTransform, pos->rotation[1], pos->localTransform);
glm_rotate_z(pos->localTransform, pos->rotation[2], pos->localTransform); if(dirty & ENTITY_POSITION_FLAG_ROTATION_DIRTY) {
glm_scale(pos->localTransform, pos->scale); // Rotation or scale changed: rebuild columns 0-2 analytically (XYZ euler order).
pos->flags &= ~ENTITY_POSITION_FLAG_LOCAL_DIRTY; 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. // Recompute worldTransform from the parent chain. Only called when WORLD_DIRTY.
static void entityPositionEnsureWorld(entityposition_t *pos) { static void entityPositionEnsureWorld(entityposition_t *pos) {
entityPositionEnsureLocal(pos);
if(!(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY)) return; if(!(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY)) return;
entityPositionEnsureLocal(pos);
if(pos->parentEntityId == ENTITY_ID_INVALID) { if(pos->parentEntityId != ENTITY_ID_INVALID) {
glm_mat4_copy(pos->localTransform, pos->worldTransform); // Parented: world = parent.world × local. worldTransform must be written
} else { // because children (and this node's getters) read it.
entityposition_t *parent = componentGetData( entityposition_t *parent = componentGetData(
pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
); );
entityPositionEnsureWorld(parent); entityPositionEnsureWorld(parent);
glm_mat4_mul(parent->worldTransform, pos->localTransform, pos->worldTransform); 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; pos->flags &= ~ENTITY_POSITION_FLAG_WORLD_DIRTY;
} }
@@ -63,15 +99,15 @@ void entityPositionInit(
entityId, componentId, COMPONENT_TYPE_POSITION 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->position);
glm_vec3_zero(pos->rotation); glm_vec3_zero(pos->rotation);
glm_vec3_one(pos->scale); glm_vec3_one(pos->scale);
glm_mat4_identity(pos->localTransform); glm_mat4_identity(pos->localTransform);
glm_mat4_identity(pos->worldTransform); glm_mat4_identity(pos->worldTransform);
pos->flags = 0;
pos->parentEntityId = ENTITY_ID_INVALID;
pos->parentComponentId = COMPONENT_ID_INVALID;
pos->childCount = 0;
} }
void entityPositionLookAt( void entityPositionLookAt(
@@ -87,7 +123,7 @@ void entityPositionLookAt(
glm_lookat(eye, target, up, pos->localTransform); glm_lookat(eye, target, up, pos->localTransform);
// localTransform is now authoritative; PRS cache is stale. // localTransform is now authoritative; PRS cache is stale.
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_PRS_DIRTY) 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); entityPositionMarkDirty(pos);
} }
@@ -100,7 +136,10 @@ void entityPositionGetTransform(
entityId, componentId, COMPONENT_TYPE_POSITION entityId, componentId, COMPONENT_TYPE_POSITION
); );
entityPositionEnsureWorld(pos); entityPositionEnsureWorld(pos);
glm_mat4_copy(pos->worldTransform, dest); glm_mat4_copy(
pos->parentEntityId == ENTITY_ID_INVALID ? pos->localTransform : pos->worldTransform,
dest
);
} }
void entityPositionGetLocalTransform( void entityPositionGetLocalTransform(
@@ -135,6 +174,12 @@ void entityPositionGetPosition(
entityposition_t *pos = componentGetData( entityposition_t *pos = componentGetData(
entityId, componentId, COMPONENT_TYPE_POSITION 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); entityPositionEnsureWorld(pos);
dest[0] = pos->worldTransform[3][0]; dest[0] = pos->worldTransform[3][0];
dest[1] = pos->worldTransform[3][1]; dest[1] = pos->worldTransform[3][1];
@@ -150,7 +195,7 @@ void entityPositionSetPosition(
entityId, componentId, COMPONENT_TYPE_POSITION entityId, componentId, COMPONENT_TYPE_POSITION
); );
glm_vec3_copy(position, pos->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; & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos); entityPositionMarkDirty(pos);
} }
@@ -176,7 +221,7 @@ void entityPositionSetRotation(
entityId, componentId, COMPONENT_TYPE_POSITION entityId, componentId, COMPONENT_TYPE_POSITION
); );
glm_vec3_copy(rotation, pos->rotation); 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; & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos); entityPositionMarkDirty(pos);
} }
@@ -202,7 +247,7 @@ void entityPositionSetScale(
entityId, componentId, COMPONENT_TYPE_POSITION entityId, componentId, COMPONENT_TYPE_POSITION
); );
glm_vec3_copy(scale, pos->scale); 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; & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos); entityPositionMarkDirty(pos);
} }
@@ -265,7 +310,7 @@ entityposition_t *entityPositionGet(
} }
void entityPositionRebuild(entityposition_t *pos) { 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; & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos); entityPositionMarkDirty(pos);
} }
@@ -324,36 +369,33 @@ void entityPositionDecompose(entityposition_t *pos) {
pos->localTransform[2][2] * pos->localTransform[2][2] pos->localTransform[2][2] * pos->localTransform[2][2]
); );
// Normalize columns to isolate the rotation matrix. // Normalize columns to isolate the rotation matrix (9 floats, no mat4 needed).
float invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f; const 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; const 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; const float invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
mat4 r; const float r00 = pos->localTransform[0][0] * invS0;
glm_mat4_identity(r); const float r01 = pos->localTransform[0][1] * invS0;
r[0][0] = pos->localTransform[0][0] * invS0; const float r02 = pos->localTransform[0][2] * invS0;
r[0][1] = pos->localTransform[0][1] * invS0; const float r10 = pos->localTransform[1][0] * invS1;
r[0][2] = pos->localTransform[0][2] * invS0; const float r11 = pos->localTransform[1][1] * invS1;
r[1][0] = pos->localTransform[1][0] * invS1; const float r20 = pos->localTransform[2][0] * invS2;
r[1][1] = pos->localTransform[1][1] * invS1; const float r21 = pos->localTransform[2][1] * invS2;
r[1][2] = pos->localTransform[1][2] * invS1; const float r22 = pos->localTransform[2][2] * invS2;
r[2][0] = pos->localTransform[2][0] * invS2;
r[2][1] = pos->localTransform[2][1] * invS2;
r[2][2] = pos->localTransform[2][2] * invS2;
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major) // 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); pos->rotation[1] = asinf(sinBeta);
float cosBeta = cosf(pos->rotation[1]); const float cosBeta = cosf(pos->rotation[1]);
if(fabsf(cosBeta) > 1e-6f) { if(fabsf(cosBeta) > 1e-6f) {
pos->rotation[0] = atan2f(-r[2][1], r[2][2]); pos->rotation[0] = atan2f(-r21, r22);
pos->rotation[2] = atan2f(-r[1][0], r[0][0]); pos->rotation[2] = atan2f(-r10, r00);
} else { } else {
// Gimbal lock: pin Z to 0, recover X. // Gimbal lock: pin Z to 0, recover X.
pos->rotation[2] = 0.0f; pos->rotation[2] = 0.0f;
pos->rotation[0] = (sinBeta > 0.0f) pos->rotation[0] = (sinBeta > 0.0f)
? atan2f(r[0][1], r[1][1]) ? atan2f(r01, r11)
: -atan2f(r[0][1], r[1][1]); : -atan2f(r01, r11);
} }
} }
@@ -15,31 +15,33 @@
* PRS cache is stale. localTransform was written directly (e.g. lookAt) and * PRS cache is stale. localTransform was written directly (e.g. lookAt) and
* position/rotation/scale need to be decomposed before they can be read. * 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 * Columns 0-2 of localTransform are stale. Rotation or scale changed; the
* be rebuilt from position/rotation/scale before it can be used. * 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 * 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. * 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 { typedef struct {
/** Local transform matrix, rebuilt lazily from position/rotation/scale. */ /*
mat4 localTransform; * Hot fields — flag checks and parent/child traversal (markDirty, ensureWorld)
/** World transform matrix, recomputed lazily from the parent chain. */ * only touch these. Kept at the front so they share the first cache line.
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;
/** Bitmask of ENTITY_POSITION_FLAG_* values describing which caches are stale. */ /** Bitmask of ENTITY_POSITION_FLAG_* values describing which caches are stale. */
uint8_t flags; uint8_t flags;
/** Entity ID of the parent node, or ENTITY_ID_INVALID if none. */ /** 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]; entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
/** Component IDs of child position components. */ /** Component IDs of child position components. */
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX]; 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; } entityposition_t;
/** /**
@@ -241,10 +264,11 @@ entityposition_t *entityPositionGet(
); );
/** /**
* Signals that the PRS cache was modified externally. Sets * Signals that the PRS cache was modified externally. Sets both
* ENTITY_POSITION_FLAG_LOCAL_DIRTY so localTransform is rebuilt lazily on the * ENTITY_POSITION_FLAG_ROTATION_DIRTY and ENTITY_POSITION_FLAG_POSITION_DIRTY
* next read, clears ENTITY_POSITION_FLAG_PRS_DIRTY, and propagates * so all of localTransform is rebuilt lazily on the next read, clears
* ENTITY_POSITION_FLAG_WORLD_DIRTY to self and all descendants. * 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. * @param pos The position component whose PRS was modified.
*/ */