mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-21 06:01:35 +02:00
HardBullet's loading frames submit only a cube layer, which the Frame's runtime doesn't composite, so the loading screen stayed black. Adapter setting cube_quads (default on) draws a cube-only frame's six faces as quad layers around the viewer; frames with a projection layer are unchanged. Adapter rebuilt on current main. Also: the foveation test expected a numeric FOVE_LEVEL to pass through (Valve's layer ignores numbers since #394), a long description line and an import order in adapter_dialog.py failed lint.
117 lines
6.6 KiB
Python
117 lines
6.6 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));
|
|
}
|
|
// 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"
|