Files
spoopyghosty0--frameport/tests/test_layer_emul.py
T
spoopyghosty0 23a47880e0 Lines within 120 columns everywhere (CI lints without exceptions); community and licence files; docs updated
- reflowed long lines (texts unchanged; generated launcher/PowerShell/README texts byte-identical)
- CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, THIRD_PARTY_NOTICES; pyproject metadata (SPDX licence, authors, urls,
  classifiers, keywords)
- maintainer-only notes moved to a git-ignored CLAUDE.local.md; CLAUDE.md facts updated
- README: what kinds of apps run and how; INSTALL: PC VR and Files wording, new labels
2026-10-02 14:18:41 -04:00

99 lines
5.5 KiB
Python

"""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 <stdio.h>
#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));
}
// 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"