"""Math behind FrameBridge's per-game fixes (native/adapter/layer_math.h): cube faces for 360° layers (equirect_emul) and the pose algebra used by stable_local / aim correction. Compiled for this host with a small C harness. Opt-in (FRAMEPORT_NATIVE_TESTS=1, -m native): needs the OpenXR headers in native/.cache (native/build.py).""" import os import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] INC = next(iter((ROOT / "native/.cache").glob("openxr-*")), None) CC = shutil.which("cc") or shutil.which("clang") pytestmark = [pytest.mark.native, pytest.mark.skipif( not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not INC or not CC, reason="set FRAMEPORT_NATIVE_TESTS=1 (needs the OpenXR headers from native/build.py and a host C compiler)")] HARNESS = r""" #include #include "layer_math.h" #define CHECK(c) do { if (!(c)) { fprintf(stderr, "FAILED line %d: %s\n", __LINE__, #c); return 1; } } while (0) #define NEAR(a, b) (fabsf((a) - (b)) < 1e-4f) #define VNEAR(v, X, Y, Z) (NEAR((v).x, X) && NEAR((v).y, Y) && NEAR((v).z, Z)) int main(void) { // face centre directions: front -Z, right +X, back +Z, left -X, up +Y, down -Y CHECK(VNEAR(lm_face_dir(0, 0, 0), 0, 0, -1)); CHECK(VNEAR(lm_face_dir(1, 0, 0), 1, 0, 0)); CHECK(VNEAR(lm_face_dir(2, 0, 0), 0, 0, 1)); CHECK(VNEAR(lm_face_dir(3, 0, 0), -1, 0, 0)); CHECK(VNEAR(lm_face_dir(4, 0, 0), 0, 1, 0)); CHECK(VNEAR(lm_face_dir(5, 0, 0), 0, -1, 0)); // faces tile seamlessly: the right edge of the front face is the left edge of the right face, etc. CHECK(VNEAR(lm_face_dir(0, 1, 0), 1, 0, -1) && VNEAR(lm_face_dir(1, -1, 0), 1, 0, -1)); CHECK(VNEAR(lm_face_dir(1, 1, 0), 1, 0, 1) && VNEAR(lm_face_dir(2, -1, 0), 1, 0, 1)); CHECK(VNEAR(lm_face_dir(0, 0, 1), 0, 1, -1) && VNEAR(lm_face_dir(4, 0, -1), 0, 1, -1)); CHECK(VNEAR(lm_face_dir(0, 0, -1), 0, -1, -1) && VNEAR(lm_face_dir(5, 0, 1), 0, -1, -1)); // every face's quad sits on its axis and faces the centre (a quad's normal is its local +Z) XrPosef origin = {{0, 0, 0, 1}, {0, 1.5f, 0}}; for (int f = 0; f < 6; ++f) { XrPosef p = lm_face_pose(origin, f, 10); XrVector3f axis = lm_face_dir(f, 0, 0); CHECK(VNEAR(p.position, 10 * axis.x, 1.5f + 10 * axis.y, 10 * axis.z)); XrVector3f normal = lm_rotate(p.orientation, (XrVector3f){0, 0, 1}); CHECK(VNEAR(normal, -axis.x, -axis.y, -axis.z)); // the quad's right (+X) is the face's +u: its right edge is where lm_face_dir(f, 1, 0) points XrVector3f right = lm_rotate(p.orientation, (XrVector3f){1, 0, 0}); XrVector3f edge = lm_face_dir(f, 1, 0); CHECK(VNEAR(edge, axis.x + right.x, axis.y + right.y, axis.z + right.z)); } // the shader's cube_dir writes every texel where GL's cube lookup will read it back for (int f = 0; f < 6; ++f) for (int i = 0; i < 5; ++i) for (int j = 0; j < 5; ++j) { float u = -0.9f + 0.45f * i, v = -0.9f + 0.45f * j, s, t; CHECK(lm_glcube_lookup(lm_glcube_dir(f, u, v), &s, &t) == f); CHECK(NEAR(s, (u + 1) / 2) && NEAR(t, (v + 1) / 2)); } // Cube layer quads: GL image s/t basis, inward normals, rotated layer, mip-0 face order. for (int f = 0; f < 6; ++f) { XrQuaternionf rotation = lm_axis_angle(0, 1, 0, 0.71f); XrPosef p = lm_cube_quad_pose(rotation, f, 50); XrVector3f axis = lm_rotate(rotation, lm_glcube_dir(f, 0, 0)); CHECK(VNEAR(p.position, 50 * axis.x, 50 * axis.y, 50 * axis.z)); XrVector3f normal = lm_rotate(p.orientation, (XrVector3f){0, 0, 1}); CHECK(VNEAR(normal, -axis.x, -axis.y, -axis.z)); for (int x = -1; x <= 1; ++x) for (int y = -1; y <= 1; ++y) { XrVector3f offset = lm_rotate(p.orientation, (XrVector3f){x, y, 0}); XrVector3f expected = lm_rotate(rotation, lm_glcube_dir(f, x, y)); CHECK(VNEAR(expected, axis.x + offset.x, axis.y + offset.y, axis.z + offset.z)); } // Vulkan reverses the image rows: source t = 1 - destination v, for the identical GL pose. float s, t; XrVector3f d = lm_glcube_dir(f, 0.4f, -0.6f); CHECK(lm_glcube_lookup(d, &s, &t) == f && NEAR(s, 0.7f) && NEAR(t, 0.2f)); } // equirect2: full sphere, image centre (u 0.5) straight ahead (-Z), +X to the right, v 1 at the top float u, v; CHECK(lm_equirect2_uv((XrVector3f){0, 0, -1}, 2 * LM_PI, LM_PI / 2, -LM_PI / 2, &u, &v) && NEAR(u, 0.5f) && NEAR(v, 0.5f)); CHECK(lm_equirect2_uv((XrVector3f){1, 0, 0}, 2 * LM_PI, LM_PI / 2, -LM_PI / 2, &u, &v) && NEAR(u, 0.75f) && NEAR(v, 0.5f)); CHECK(lm_equirect2_uv((XrVector3f){-1, 0, 0}, 2 * LM_PI, LM_PI / 2, -LM_PI / 2, &u, &v) && NEAR(u, 0.25f)); CHECK(lm_equirect2_uv((XrVector3f){0, 1, 0}, 2 * LM_PI, LM_PI / 2, -LM_PI / 2, &u, &v) && NEAR(v, 1.0f)); CHECK(lm_equirect2_uv((XrVector3f){0, -1, -0.0001f}, 2 * LM_PI, LM_PI / 2, -LM_PI / 2, &u, &v) && v < 0.001f); // a 180° layer covers the front hemisphere only CHECK(lm_equirect2_uv((XrVector3f){0.9f, 0, -0.1f}, LM_PI, LM_PI / 2, -LM_PI / 2, &u, &v) && u > 0.9f); CHECK(!lm_equirect2_uv((XrVector3f){0, 0, 1}, LM_PI, LM_PI / 2, -LM_PI / 2, &u, &v)); // face choice: looking ahead with a ~60° half-diagonal FOV + margin, the back face is never needed float cos_limit = cosf(1.05f + 0.26f); XrVector3f ahead = {0, 0, -1}; CHECK(lm_face_visible(0, ahead, cos_limit) && !lm_face_visible(2, ahead, cos_limit)); CHECK(lm_face_visible(1, ahead, cos_limit) && lm_face_visible(4, ahead, cos_limit)); CHECK(lm_face_score(0, ahead) > lm_face_score(1, ahead)); // pose algebra (stable_local): (A∘B)∘B⁻¹ = A XrPosef a = {lm_axis_angle(0, 1, 0, 0.7f), {0.3f, -0.2f, 1.1f}}, b = {lm_axis_angle(1, 0, 0, -0.4f), {-0.5f, 0.1f, 0.2f}}; XrPosef back = lm_pose_mul(lm_pose_mul(a, b), lm_pose_inv(b)); CHECK(VNEAR(back.position, a.position.x, a.position.y, a.position.z)); CHECK(NEAR(fabsf(back.orientation.w), fabsf(a.orientation.w)) && NEAR(lm_angle(lm_qmul(back.orientation, lm_qconj(a.orientation))), 0)); CHECK(NEAR(lm_angle(lm_axis_angle(0, 1, 0, 0.5f)), 0.5f)); puts("ok"); return 0; } """ def test_layer_math(tmp_path): src = tmp_path / "harness.c" src.write_text(HARNESS) exe = tmp_path / "harness" subprocess.run([CC, "-O1", "-Wall", "-Wextra", "-Werror", "-I", str(INC), "-I", str(ROOT / "native/adapter"), str(src), "-lm", "-o", str(exe)], check=True) out = subprocess.run([str(exe)], capture_output=True, text=True) assert out.returncode == 0, out.stderr assert out.stdout.strip() == "ok"