Time fixes

This commit is contained in:
2026-07-18 20:31:42 -05:00
parent a618ff46fe
commit 1174e471f6
16 changed files with 676 additions and 22 deletions
+1
View File
@@ -8,3 +8,4 @@ include(dusktest)
# Tests
dusktest(test_physicstest.c)
dusktest(test_physicsworld.c)
dusktest(test_physicsshapemesh.c)
+185
View File
@@ -0,0 +1,185 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "physics/physicstest.h"
#include "physics/physicsshapemesh.h"
// A flat 10x10 ground quad on the XZ plane at y=0, centered at the origin
// (two triangles, tightly packed vec3 vertices -- no indices).
static vec3 FLAT_GROUND_VERTICES[6] = {
{ -5.0f, 0.0f, -5.0f }, { 5.0f, 0.0f, -5.0f }, { 5.0f, 0.0f, 5.0f },
{ -5.0f, 0.0f, -5.0f }, { 5.0f, 0.0f, 5.0f }, { -5.0f, 0.0f, 5.0f },
};
static physicsshapemesh_t flatGroundMesh(void) {
return (physicsshapemesh_t){
.vertices = (const uint8_t *)FLAT_GROUND_VERTICES,
.stride = sizeof(vec3),
.triangleCount = 2
};
}
static void test_getVertexReadsRawPositions(void **state) {
physicsshapemesh_t mesh = flatGroundMesh();
vec3 v0, v3;
physicsShapeMeshGetVertex(&mesh, 0, v0);
physicsShapeMeshGetVertex(&mesh, 3, v3);
assert_float_equal(v0[0], -5.0f, 0.0001f);
assert_float_equal(v0[2], -5.0f, 0.0001f);
assert_float_equal(v3[0], -5.0f, 0.0001f);
assert_float_equal(v3[2], -5.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// Vertex layout matching a typical interleaved render vertex (uv then
// pos), to prove the stride-based reader can point straight at a render
// buffer instead of a dedicated physics copy.
typedef struct {
float_t uv[2];
float_t pos[3];
} testInterleavedVertex_t;
static void test_supportsInterleavedRenderVertexLayout(void **state) {
testInterleavedVertex_t vertices[3] = {
{ .uv = { 0, 0 }, .pos = { -5.0f, 0.0f, -5.0f } },
{ .uv = { 1, 0 }, .pos = { 5.0f, 0.0f, -5.0f } },
{ .uv = { 0, 1 }, .pos = { -5.0f, 0.0f, 5.0f } },
};
physicsshapemesh_t mesh = {
.vertices = (const uint8_t *)vertices[0].pos,
.stride = sizeof(testInterleavedVertex_t),
.triangleCount = 1
};
vec3 v1;
physicsShapeMeshGetVertex(&mesh, 1, v1);
assert_float_equal(v1[0], 5.0f, 0.0001f);
assert_float_equal(v1[2], -5.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sphereRestingOnMesh(void **state) {
physicsshapemesh_t mesh = flatGroundMesh();
vec3 meshPos = { 0.0f, 0.0f, 0.0f };
// Sphere overlapping the ground by 0.2.
vec3 sphereCenter = { 0.0f, 0.3f, 0.0f };
vec3 normal; float_t depth;
assert_true(physicsShapeMeshTestSphere(
&mesh, meshPos, sphereCenter, 0.5f, normal, &depth
));
assert_float_equal(depth, 0.2f, 0.0001f);
assert_float_equal(normal[1], 1.0f, 0.0001f); // pushes sphere up
// Far above: no overlap.
vec3 sphereFar = { 0.0f, 10.0f, 0.0f };
assert_false(physicsShapeMeshTestSphere(
&mesh, meshPos, sphereFar, 0.5f, normal, &depth
));
// Offsetting the mesh's own position shifts the ground with it: raising
// both the ground and the sphere together reproduces the same overlap.
vec3 meshPosRaised = { 0.0f, 1.0f, 0.0f };
vec3 sphereCenterRaised = { 0.0f, 1.3f, 0.0f };
assert_true(physicsShapeMeshTestSphere(
&mesh, meshPosRaised, sphereCenterRaised, 0.5f, normal, &depth
));
assert_float_equal(depth, 0.2f, 0.0001f);
// But the sphere no longer overlaps the ground at its old (un-raised)
// height, since the ground moved out from under it.
assert_false(physicsShapeMeshTestSphere(
&mesh, meshPosRaised, sphereCenter, 0.5f, normal, &depth
));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_capsuleRestingOnMesh(void **state) {
physicsshapemesh_t mesh = flatGroundMesh();
vec3 meshPos = { 0.0f, 0.0f, 0.0f };
// Standing capsule (half-height 1) whose bottom cap dips 0.1 into the
// ground: center at y = radius + halfHeight - 0.1.
const float_t radius = 0.5f;
const float_t halfHeight = 1.0f;
vec3 capsuleCenter = { 0.0f, radius + halfHeight - 0.1f, 0.0f };
vec3 normal; float_t depth;
assert_true(physicsShapeMeshTestCapsule(
&mesh, meshPos, capsuleCenter, radius, halfHeight, normal, &depth
));
assert_float_equal(depth, 0.1f, 0.001f);
assert_float_equal(normal[1], 1.0f, 0.0001f);
// Lifted well clear of the ground: no overlap.
vec3 capsuleHigh = { 0.0f, 10.0f, 0.0f };
assert_false(physicsShapeMeshTestCapsule(
&mesh, meshPos, capsuleHigh, radius, halfHeight, normal, &depth
));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_cubeIsUnsupported(void **state) {
physicsshapemesh_t mesh = flatGroundMesh();
vec3 meshPos = { 0.0f, 0.0f, 0.0f };
physicsshape_t cube = {
.type = PHYSICS_SHAPE_CUBE,
.data.cube.halfExtents = { 0.5f, 0.5f, 0.5f }
};
vec3 cubePos = { 0.0f, 0.3f, 0.0f };
vec3 normal; float_t depth;
assert_false(physicsShapeMeshTest(
meshPos, &mesh, cubePos, &cube, normal, &depth
));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsShapeMeshCreateIntegratesWithDispatch(void **state) {
physicsshapemesh_t mesh = flatGroundMesh();
physicsshape_t landscape = physicsShapeMeshCreate(&mesh);
assert_int_equal(landscape.type, PHYSICS_SHAPE_CUSTOM);
physicsshape_t sphere = {
.type = PHYSICS_SHAPE_SPHERE,
.data.sphere.radius = 0.5f
};
vec3 meshPos = { 0.0f, 0.0f, 0.0f };
vec3 sphereCenter = { 0.0f, 0.3f, 0.0f };
// Landscape as B, matching physicsWorldStep's dynamic(A)-vs-static(B).
vec3 normal; float_t depth;
assert_true(physicsTestShapeVsShape(
sphereCenter, sphere, meshPos, landscape, normal, &depth
));
assert_float_equal(depth, 0.2f, 0.0001f);
assert_float_equal(normal[1], 1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_getVertexReadsRawPositions),
cmocka_unit_test(test_supportsInterleavedRenderVertexLayout),
cmocka_unit_test(test_sphereRestingOnMesh),
cmocka_unit_test(test_capsuleRestingOnMesh),
cmocka_unit_test(test_cubeIsUnsupported),
cmocka_unit_test(test_physicsShapeMeshCreateIntegratesWithDispatch),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+44 -5
View File
@@ -12,6 +12,7 @@
#include "entity/entitymanager.h"
static void countingUpdateCallback(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
@@ -58,10 +59,9 @@ static void test_sceneEntitiesAreIsolatedPerScene(void **state) {
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneUpdateOnlyTicksActiveScene(void **state) {
static void test_sceneFixedUpdateOnlyTicksActiveScene(void **state) {
sceneInit();
timeInit();
TIME.dynamicUpdate = true;
sceneid_t sceneA = sceneCreate();
sceneid_t sceneB = sceneCreate();
@@ -84,13 +84,13 @@ static void test_sceneUpdateOnlyTicksActiveScene(void **state) {
);
sceneSetActive(sceneA);
sceneUpdate();
sceneFixedUpdate();
assert_int_equal(counterA, 1);
assert_int_equal(counterB, 0);
sceneSetActive(sceneB);
sceneUpdate();
sceneFixedUpdate();
assert_int_equal(counterA, 1);
assert_int_equal(counterB, 1);
@@ -99,6 +99,44 @@ static void test_sceneUpdateOnlyTicksActiveScene(void **state) {
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames(void **state) {
sceneInit();
timeInit();
sceneid_t scene = sceneCreate();
sceneSetActive(scene);
uint32_t counter = 0;
entitymanager_t *entities = sceneGetEntities(scene);
entityid_t entity = entityManagerAdd(entities);
entityUpdateAdd(
entities, entity, countingUpdateCallback, COMPONENT_ID_INVALID, &counter
);
#if DUSK_TIME_DYNAMIC
// Still accumulating toward the next fixed timestep: sceneUpdate must
// not run sceneFixedUpdate yet.
TIME.dynamicUpdate = true;
sceneUpdate();
assert_int_equal(counter, 0);
// A fixed timestep boundary was just crossed: sceneUpdate must run
// sceneFixedUpdate exactly once.
TIME.dynamicUpdate = false;
sceneUpdate();
assert_int_equal(counter, 1);
#else
// Non-dynamic-time platforms: every frame is a fixed timestep.
sceneUpdate();
assert_int_equal(counter, 1);
sceneUpdate();
assert_int_equal(counter, 2);
#endif
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneDestroyClearsActive(void **state) {
sceneInit();
@@ -117,7 +155,8 @@ int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_sceneCreateDestroy),
cmocka_unit_test(test_sceneEntitiesAreIsolatedPerScene),
cmocka_unit_test(test_sceneUpdateOnlyTicksActiveScene),
cmocka_unit_test(test_sceneFixedUpdateOnlyTicksActiveScene),
cmocka_unit_test(test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames),
cmocka_unit_test(test_sceneDestroyClearsActive),
};