Add VITURE glasses support: hardware control, 3D, head tracking

Glasses get their own controller over the MCU (permission, 120 Hz default, brightness, volume, sunblock, 2D/3D) and the game moves onto them when plugged in. The IMU drives Cardboard head tracking and the SteamVR driver. 3D falls back to one eye's view on phones that can't send the 3840x1080 signal. Options open from a glasses button in game and on the Steam page.
This commit is contained in:
maxjivi05 committed 2026-10-13 11:24:01 -04:00
1 parent 31d12a9f6c
commit 1e66ff17c1
37 files changed
+2624 -318

No files matched your search

+15 -9
View File
@@ -43,6 +43,8 @@ void cardboard_clamp(struct cardboard_params *p) {
p->fov_deg = clampf(p->fov_deg, 30.0f, 140.0f);
p->lens_inset = clampf(p->lens_inset, -0.25f, 0.25f);
p->chroma = clampf(p->chroma, 0.0f, 0.05f);
p->mono = p->mono ? 1 : 0;
p->display_hz = clampf(p->display_hz, 0.0f, 240.0f);
}
void cardboard_push_constants(const struct cardboard_params *p, const float pose[9],
@@ -50,7 +52,7 @@ void cardboard_push_constants(const struct cardboard_params *p, const float pose
/* rot0..rot2: the pose's rows, each with one scalar riding in w. */
for (int r = 0; r < 3; r++)
for (int c = 0; c < 3; c++) out[r * 4 + c] = pose[r * 3 + c];
out[3] = p->ipd_m * 0.5f;
out[3] = p->mono ? 0.0f : p->ipd_m * 0.5f;
out[7] = p->screen_dist_m;
out[11] = p->screen_width_m * 0.5f;
/* lens */
@@ -63,8 +65,9 @@ void cardboard_push_constants(const struct cardboard_params *p, const float pose
out[17] = (src_w > 0 && src_h > 0) ? (float)src_w / (float)src_h : 16.0f / 9.0f;
if (p->source == CARDBOARD_SOURCE_SBS) out[17] *= 0.5f; /* one eye's half */
out[18] = (float)p->source;
out[19] = (out_w > 0 && out_h > 0) ? ((float)out_w * 0.5f) / (float)out_h : 1.0f;
out[20] = out[21] = out[22] = out[23] = 0.0f; /* reserved */
out[19] = (out_w > 0 && out_h > 0) ? ((float)out_w * (p->mono ? 1.0f : 0.5f)) / (float)out_h : 1.0f;
out[20] = p->mono ? 1.0f : 0.0f;
out[21] = out[22] = out[23] = 0.0f; /* reserved */
}
#ifndef CARDBOARD_HOST_TEST
@@ -91,9 +94,9 @@ void vkp_cardboard_set(const struct cardboard_params *p) {
shm_publish(g_live);
pthread_mutex_unlock(&g_lock);
if (was != c.enabled || c.enabled)
droiddeck_log("cardboard", "%s: %s, ipd %.0f mm, screen %.1f m wide at %.1f m, lens k1 %.2f k2 %.2f, fov %.0f, inset %.3f, chroma %.3f",
droiddeck_log("cardboard", "%s: %s%s, ipd %.0f mm, screen %.1f m wide at %.1f m, lens k1 %.2f k2 %.2f, fov %.0f, inset %.3f, chroma %.3f",
c.enabled ? "on" : "off", c.source == CARDBOARD_SOURCE_SBS ? "side-by-side source" : "virtual screen",
c.ipd_m * 1000.0f, c.screen_width_m, c.screen_dist_m, c.k1, c.k2, c.fov_deg, c.lens_inset, c.chroma);
c.mono ? " (one eye)" : "", c.ipd_m * 1000.0f, c.screen_width_m, c.screen_dist_m, c.k1, c.k2, c.fov_deg, c.lens_inset, c.chroma);
}
/* The guest's copy of the pose (headpose_shm.h), mapped once a path is known. */
@@ -131,7 +134,9 @@ static void shm_publish(int live) {
atomic_thread_fence(memory_order_release);
memcpy(g_shm->rot, g_pose, sizeof(g_shm->rot));
g_shm->ipd_m = g_set.ipd_m;
g_shm->flags = (live ? DROIDDECK_HEADPOSE_LIVE : 0u) | (g_set.enabled ? DROIDDECK_HEADPOSE_ENABLED : 0u);
g_shm->flags = (live ? DROIDDECK_HEADPOSE_LIVE : 0u) | (g_set.enabled ? DROIDDECK_HEADPOSE_ENABLED : 0u) |
(g_set.mono ? DROIDDECK_HEADPOSE_MONO : 0u);
g_shm->display_hz = g_set.display_hz;
g_shm->time_ns = (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
g_shm->tan_half_fov = tanf(g_set.fov_deg * 0.5f * (float)M_PI / 180.0f);
g_shm->eye_w = (uint32_t)(g_eye_w > 0 ? g_eye_w : 0);
@@ -418,14 +423,15 @@ VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h
if (!said) { said = 1; droiddeck_log("error", "cardboard: no descriptor for the frame; shown flat"); }
return VK_NULL_HANDLE;
}
if (g_eye_w != out_w / 2 || g_eye_h != out_h) {
snapshot();
int eye_w = g_cur.mono ? out_w : out_w / 2;
if (g_eye_w != eye_w || g_eye_h != out_h) {
pthread_mutex_lock(&g_lock);
g_eye_w = out_w / 2;
g_eye_w = eye_w;
g_eye_h = out_h;
shm_publish(g_live);
pthread_mutex_unlock(&g_lock);
}
snapshot();
struct { float ndc[4]; float rest[24]; } pc = {{-1.0f, -1.0f, 1.0f, 1.0f}, {0}};
cardboard_push_constants(&g_cur, g_cur_pose, src_w, src_h, out_w, out_h, pc.rest);
@@ -24,6 +24,7 @@ layout(push_constant) uniform PC {
vec4 lens; // x: k1, y: k2, z: tan of half the eye's vertical field of view, w: chromatic spread
vec4 view; // x: lens centre inset (fraction of an eye viewport), y: source aspect w/h,
// z: source mode, w: eye viewport aspect w/h
vec4 extra;
} pc;
const vec3 BACKDROP = vec3(0.015, 0.015, 0.02);
@@ -59,10 +60,11 @@ vec3 eyeSample(vec2 tan, float eye) {
}
void main() {
float eye = uv.x < 0.5 ? 0.0 : 1.0;
vec2 local = vec2(uv.x * 2.0 - eye, uv.y);
bool mono = pc.extra.x > 0.5;
float eye = mono || uv.x < 0.5 ? 0.0 : 1.0;
vec2 local = mono ? uv : vec2(uv.x * 2.0 - eye, uv.y);
// Lens centres sit inset towards the nose by view.x of a viewport.
float cx = 0.5 + (eye < 0.5 ? pc.view.x : -pc.view.x);
float cx = mono ? 0.5 : 0.5 + (eye < 0.5 ? pc.view.x : -pc.view.x);
vec2 p = local - vec2(cx, 0.5);
float tanH = pc.lens.z;
vec2 t = vec2(p.x * pc.view.w, p.y) * 2.0 * tanH;
@@ -30,6 +30,8 @@ struct cardboard_params {
float fov_deg; /* vertical field of view of one eye before distortion */
float lens_inset; /* lens centre inset towards the nose, fraction of an eye viewport */
float chroma; /* chromatic aberration pre-correction (0 = off) */
int mono;
float display_hz;
};
/* Any thread. Values are clamped to sane ranges. */
+272 -254
View File
@@ -1,9 +1,9 @@
// 1116.2.0
// 1115.1.0
#pragma once
const uint32_t cardboard_frag_code[] = {
0x07230203,0x00010000,0x0008000b,0x0000016d,0x00000000,0x00020011,0x00000001,0x0006000b,
0x07230203,0x00010000,0x0008000b,0x00000185,0x00000000,0x00020011,0x00000001,0x0006000b,
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x000000ee,0x00000165,0x00030010,
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x000000f8,0x0000017d,0x00030010,
0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,
0x00000000,0x00070005,0x0000000d,0x69646e75,0x726f7473,0x66762874,0x31663b32,0x0000003b,
0x00030005,0x0000000b,0x00000074,0x00040005,0x0000000c,0x6c616373,0x00000065,0x00070005,
@@ -12,255 +12,273 @@ const uint32_t cardboard_frag_code[] = {
0x0000001e,0x00004350,0x00040006,0x0000001e,0x00000000,0x0063646e,0x00050006,0x0000001e,
0x00000001,0x30746f72,0x00000000,0x00050006,0x0000001e,0x00000002,0x31746f72,0x00000000,
0x00050006,0x0000001e,0x00000003,0x32746f72,0x00000000,0x00050006,0x0000001e,0x00000004,
0x736e656c,0x00000000,0x00050006,0x0000001e,0x00000005,0x77656976,0x00000000,0x00030005,
0x00000020,0x00006370,0x00040005,0x0000003f,0x486e6174,0x00000000,0x00030005,0x00000042,
0x00000073,0x00040005,0x00000068,0x72756f53,0x00006563,0x00030005,0x00000077,0x00000052,
0x00030005,0x00000094,0x00000064,0x00030005,0x0000009f,0x0000006f,0x00030005,0x000000b3,
0x00000074,0x00030005,0x000000bd,0x00746968,0x00040005,0x000000c3,0x666c6168,0x00000057,
0x00040005,0x000000c6,0x666c6168,0x00000048,0x00030005,0x000000cb,0x00000073,0x00030005,
0x000000ec,0x00657965,0x00030005,0x000000ee,0x00007675,0x00040005,0x000000f4,0x61636f6c,
0x0000006c,0x00030005,0x000000fe,0x00007863,0x00030005,0x0000010c,0x00000070,0x00040005,
0x00000111,0x486e6174,0x00000000,0x00030005,0x00000114,0x00000074,0x00030005,0x00000125,
0x00000063,0x00040005,0x00000129,0x61726170,0x0000006d,0x00040005,0x0000012b,0x61726170,
0x0000006d,0x00040005,0x0000012d,0x61726170,0x0000006d,0x00040005,0x0000012e,0x61726170,
0x0000006d,0x00040005,0x00000133,0x61726170,0x0000006d,0x00040005,0x00000135,0x61726170,
0x0000006d,0x00040005,0x00000137,0x61726170,0x0000006d,0x00040005,0x00000138,0x61726170,
0x0000006d,0x00040005,0x00000140,0x61726170,0x0000006d,0x00040005,0x00000142,0x61726170,
0x0000006d,0x00040005,0x00000144,0x61726170,0x0000006d,0x00040005,0x00000145,0x61726170,
0x0000006d,0x00040005,0x0000014b,0x61726170,0x0000006d,0x00040005,0x0000014d,0x61726170,
0x0000006d,0x00040005,0x0000014f,0x61726170,0x0000006d,0x00040005,0x00000150,0x61726170,
0x0000006d,0x00030005,0x00000153,0x00000065,0x00040005,0x00000159,0x65646166,0x00000000,
0x00050005,0x00000165,0x4374756f,0x726f6c6f,0x00000000,0x00030047,0x0000001e,0x00000002,
0x00050048,0x0000001e,0x00000000,0x00000023,0x00000000,0x00050048,0x0000001e,0x00000001,
0x00000023,0x00000010,0x00050048,0x0000001e,0x00000002,0x00000023,0x00000020,0x00050048,
0x0000001e,0x00000003,0x00000023,0x00000030,0x00050048,0x0000001e,0x00000004,0x00000023,
0x00000040,0x00050048,0x0000001e,0x00000005,0x00000023,0x00000050,0x00040047,0x00000068,
0x00000021,0x00000000,0x00040047,0x00000068,0x00000022,0x00000000,0x00040047,0x000000ee,
0x0000001e,0x00000000,0x00040047,0x00000165,0x0000001e,0x00000000,0x00020013,0x00000002,
0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,
0x00000006,0x00000002,0x00040020,0x00000008,0x00000007,0x00000007,0x00040020,0x00000009,
0x00000007,0x00000006,0x00050021,0x0000000a,0x00000007,0x00000008,0x00000009,0x00040017,
0x0000000f,0x00000006,0x00000003,0x00050021,0x00000010,0x0000000f,0x00000008,0x00000009,
0x0004002b,0x00000006,0x0000001c,0x3f800000,0x00040017,0x0000001d,0x00000006,0x00000004,
0x0008001e,0x0000001e,0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x0000001d,
0x00040020,0x0000001f,0x00000009,0x0000001e,0x0004003b,0x0000001f,0x00000020,0x00000009,
0x00040015,0x00000021,0x00000020,0x00000001,0x0004002b,0x00000021,0x00000022,0x00000004,
0x00040015,0x00000023,0x00000020,0x00000000,0x0004002b,0x00000023,0x00000024,0x00000000,
0x00040020,0x00000025,0x00000009,0x00000006,0x0004002b,0x00000023,0x0000002b,0x00000001,
0x0004002b,0x00000021,0x00000036,0x00000005,0x0004002b,0x00000023,0x00000037,0x00000002,
0x0004002b,0x00000006,0x0000003a,0x3f000000,0x00020014,0x0000003b,0x0004002b,0x00000023,
0x00000046,0x00000003,0x0004002b,0x00000006,0x00000054,0x00000000,0x0005002c,0x00000007,
0x00000055,0x00000054,0x00000054,0x00040017,0x00000056,0x0000003b,0x00000002,0x0005002c,
0x00000007,0x0000005d,0x0000001c,0x0000001c,0x0006002c,0x0000000f,0x00000063,0x00000054,
0x00000054,0x00000054,0x00090019,0x00000065,0x00000006,0x00000001,0x00000000,0x00000000,
0x00000000,0x00000001,0x00000000,0x0003001b,0x00000066,0x00000065,0x00040020,0x00000067,
0x00000000,0x00000066,0x0004003b,0x00000067,0x00000068,0x00000000,0x00040018,0x00000075,
0x0000000f,0x00000003,0x00040020,0x00000076,0x00000007,0x00000075,0x0004002b,0x00000021,
0x00000078,0x00000001,0x00040020,0x00000079,0x00000009,0x0000001d,0x0004002b,0x00000021,
0x0000007d,0x00000002,0x0004002b,0x00000021,0x00000081,0x00000003,0x00040020,0x00000093,
0x00000007,0x0000000f,0x0004002b,0x00000006,0x0000009b,0xbf800000,0x0004002b,0x00000006,
0x000000ab,0xb8d1b717,0x0004002b,0x00000006,0x000000af,0x3c75c28f,0x0004002b,0x00000006,
0x000000b0,0x3ca3d70a,0x0006002c,0x0000000f,0x000000b1,0x000000af,0x000000af,0x000000b0,
0x00040020,0x000000ed,0x00000001,0x00000007,0x0004003b,0x000000ed,0x000000ee,0x00000001,
0x00040020,0x000000ef,0x00000001,0x00000006,0x0004002b,0x00000006,0x000000f7,0x40000000,
0x0004002b,0x00000006,0x0000015a,0x3cf5c28f,0x00040020,0x00000164,0x00000003,0x0000001d,
0x0004003b,0x00000164,0x00000165,0x00000003,0x00050036,0x00000002,0x00000004,0x00000000,
0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000009,0x000000ec,0x00000007,0x0004003b,
0x00000008,0x000000f4,0x00000007,0x0004003b,0x00000009,0x000000fe,0x00000007,0x0004003b,
0x00000009,0x00000101,0x00000007,0x0004003b,0x00000008,0x0000010c,0x00000007,0x0004003b,
0x00000009,0x00000111,0x00000007,0x0004003b,0x00000008,0x00000114,0x00000007,0x0004003b,
0x00000093,0x00000125,0x00000007,0x0004003b,0x00000008,0x00000129,0x00000007,0x0004003b,
0x00000009,0x0000012b,0x00000007,0x0004003b,0x00000008,0x0000012d,0x00000007,0x0004003b,
0x00000009,0x0000012e,0x00000007,0x0004003b,0x00000008,0x00000133,0x00000007,0x0004003b,
0x00000009,0x00000135,0x00000007,0x0004003b,0x00000008,0x00000137,0x00000007,0x0004003b,
0x00000009,0x00000138,0x00000007,0x0004003b,0x00000008,0x00000140,0x00000007,0x0004003b,
0x00000009,0x00000142,0x00000007,0x0004003b,0x00000008,0x00000144,0x00000007,0x0004003b,
0x00000009,0x00000145,0x00000007,0x0004003b,0x00000008,0x0000014b,0x00000007,0x0004003b,
0x00000009,0x0000014d,0x00000007,0x0004003b,0x00000008,0x0000014f,0x00000007,0x0004003b,
0x00000009,0x00000150,0x00000007,0x0004003b,0x00000008,0x00000153,0x00000007,0x0004003b,
0x00000009,0x00000159,0x00000007,0x00050041,0x000000ef,0x000000f0,0x000000ee,0x00000024,
0x0004003d,0x00000006,0x000000f1,0x000000f0,0x000500b8,0x0000003b,0x000000f2,0x000000f1,
0x0000003a,0x000600a9,0x00000006,0x000000f3,0x000000f2,0x00000054,0x0000001c,0x0003003e,
0x000000ec,0x000000f3,0x00050041,0x000000ef,0x000000f5,0x000000ee,0x00000024,0x0004003d,
0x00000006,0x000000f6,0x000000f5,0x00050085,0x00000006,0x000000f8,0x000000f6,0x000000f7,
0x0004003d,0x00000006,0x000000f9,0x000000ec,0x00050083,0x00000006,0x000000fa,0x000000f8,
0x000000f9,0x00050041,0x000000ef,0x000000fb,0x000000ee,0x0000002b,0x0004003d,0x00000006,
0x000000fc,0x000000fb,0x00050050,0x00000007,0x000000fd,0x000000fa,0x000000fc,0x0003003e,
0x000000f4,0x000000fd,0x0004003d,0x00000006,0x000000ff,0x000000ec,0x000500b8,0x0000003b,
0x00000100,0x000000ff,0x0000003a,0x000300f7,0x00000103,0x00000000,0x000400fa,0x00000100,
0x00000102,0x00000106,0x000200f8,0x00000102,0x00060041,0x00000025,0x00000104,0x00000020,
0x00000036,0x00000024,0x0004003d,0x00000006,0x00000105,0x00000104,0x0003003e,0x00000101,
0x00000105,0x000200f9,0x00000103,0x000200f8,0x00000106,0x00060041,0x00000025,0x00000107,
0x00000020,0x00000036,0x00000024,0x0004003d,0x00000006,0x00000108,0x00000107,0x0004007f,
0x00000006,0x00000109,0x00000108,0x0003003e,0x00000101,0x00000109,0x000200f9,0x00000103,
0x000200f8,0x00000103,0x0004003d,0x00000006,0x0000010a,0x00000101,0x00050081,0x00000006,
0x0000010b,0x0000003a,0x0000010a,0x0003003e,0x000000fe,0x0000010b,0x0004003d,0x00000007,
0x0000010d,0x000000f4,0x0004003d,0x00000006,0x0000010e,0x000000fe,0x00050050,0x00000007,
0x0000010f,0x0000010e,0x0000003a,0x00050083,0x00000007,0x00000110,0x0000010d,0x0000010f,
0x0003003e,0x0000010c,0x00000110,0x00060041,0x00000025,0x00000112,0x00000020,0x00000022,
0x00000037,0x0004003d,0x00000006,0x00000113,0x00000112,0x0003003e,0x00000111,0x00000113,
0x00050041,0x00000009,0x00000115,0x0000010c,0x00000024,0x0004003d,0x00000006,0x00000116,
0x00000115,0x00060041,0x00000025,0x00000117,0x00000020,0x00000036,0x00000046,0x0004003d,
0x00000006,0x00000118,0x00000117,0x00050085,0x00000006,0x00000119,0x00000116,0x00000118,
0x00050041,0x00000009,0x0000011a,0x0000010c,0x0000002b,0x0004003d,0x00000006,0x0000011b,
0x0000011a,0x00050050,0x00000007,0x0000011c,0x00000119,0x0000011b,0x0005008e,0x00000007,
0x0000011d,0x0000011c,0x000000f7,0x0004003d,0x00000006,0x0000011e,0x00000111,0x0005008e,
0x00000007,0x0000011f,0x0000011d,0x0000011e,0x0003003e,0x00000114,0x0000011f,0x00060041,
0x00000025,0x00000120,0x00000020,0x00000022,0x00000046,0x0004003d,0x00000006,0x00000121,
0x00000120,0x000500ba,0x0000003b,0x00000122,0x00000121,0x00000054,0x000300f7,0x00000124,
0x00000000,0x000400fa,0x00000122,0x00000123,0x0000014a,0x000200f8,0x00000123,0x00060041,
0x00000025,0x00000126,0x00000020,0x00000022,0x00000046,0x0004003d,0x00000006,0x00000127,
0x00000126,0x00050083,0x00000006,0x00000128,0x0000001c,0x00000127,0x0004003d,0x00000007,
0x0000012a,0x00000114,0x0003003e,0x00000129,0x0000012a,0x0003003e,0x0000012b,0x00000128,
0x00060039,0x00000007,0x0000012c,0x0000000d,0x00000129,0x0000012b,0x0003003e,0x0000012d,
0x0000012c,0x0004003d,0x00000006,0x0000012f,0x000000ec,0x0003003e,0x0000012e,0x0000012f,
0x00060039,0x0000000f,0x00000130,0x00000013,0x0000012d,0x0000012e,0x00050051,0x00000006,
0x00000131,0x00000130,0x00000000,0x00050041,0x00000009,0x00000132,0x00000125,0x00000024,
0x0003003e,0x00000132,0x00000131,0x0004003d,0x00000007,0x00000134,0x00000114,0x0003003e,
0x00000133,0x00000134,0x0003003e,0x00000135,0x0000001c,0x00060039,0x00000007,0x00000136,
0x0000000d,0x00000133,0x00000135,0x0003003e,0x00000137,0x00000136,0x0004003d,0x00000006,
0x00000139,0x000000ec,0x0003003e,0x00000138,0x00000139,0x00060039,0x0000000f,0x0000013a,
0x00000013,0x00000137,0x00000138,0x00050051,0x00000006,0x0000013b,0x0000013a,0x00000001,
0x00050041,0x00000009,0x0000013c,0x00000125,0x0000002b,0x0003003e,0x0000013c,0x0000013b,
0x00060041,0x00000025,0x0000013d,0x00000020,0x00000022,0x00000046,0x0004003d,0x00000006,
0x0000013e,0x0000013d,0x00050081,0x00000006,0x0000013f,0x0000001c,0x0000013e,0x0004003d,
0x00000007,0x00000141,0x00000114,0x0003003e,0x00000140,0x00000141,0x0003003e,0x00000142,
0x0000013f,0x00060039,0x00000007,0x00000143,0x0000000d,0x00000140,0x00000142,0x0003003e,
0x00000144,0x00000143,0x0004003d,0x00000006,0x00000146,0x000000ec,0x0003003e,0x00000145,
0x00000146,0x00060039,0x0000000f,0x00000147,0x00000013,0x00000144,0x00000145,0x00050051,
0x00000006,0x00000148,0x00000147,0x00000002,0x00050041,0x00000009,0x00000149,0x00000125,
0x00000037,0x0003003e,0x00000149,0x00000148,0x000200f9,0x00000124,0x000200f8,0x0000014a,
0x0004003d,0x00000007,0x0000014c,0x00000114,0x0003003e,0x0000014b,0x0000014c,0x0003003e,
0x0000014d,0x0000001c,0x00060039,0x00000007,0x0000014e,0x0000000d,0x0000014b,0x0000014d,
0x0003003e,0x0000014f,0x0000014e,0x0004003d,0x00000006,0x00000151,0x000000ec,0x0003003e,
0x00000150,0x00000151,0x00060039,0x0000000f,0x00000152,0x00000013,0x0000014f,0x00000150,
0x0003003e,0x00000125,0x00000152,0x000200f9,0x00000124,0x000200f8,0x00000124,0x0004003d,
0x00000007,0x00000154,0x000000f4,0x0004003d,0x00000007,0x00000155,0x000000f4,0x00050050,
0x00000007,0x00000156,0x0000001c,0x0000001c,0x00050083,0x00000007,0x00000157,0x00000156,
0x00000155,0x0007000c,0x00000007,0x00000158,0x00000001,0x00000025,0x00000154,0x00000157,
0x0003003e,0x00000153,0x00000158,0x00050041,0x00000009,0x0000015b,0x00000153,0x00000024,
0x0004003d,0x00000006,0x0000015c,0x0000015b,0x00060041,0x00000025,0x0000015d,0x00000020,
0x00000036,0x00000046,0x0004003d,0x00000006,0x0000015e,0x0000015d,0x00050085,0x00000006,
0x0000015f,0x0000015c,0x0000015e,0x00050041,0x00000009,0x00000160,0x00000153,0x0000002b,
0x0004003d,0x00000006,0x00000161,0x00000160,0x0007000c,0x00000006,0x00000162,0x00000001,
0x00000025,0x0000015f,0x00000161,0x0008000c,0x00000006,0x00000163,0x00000001,0x00000031,
0x00000054,0x0000015a,0x00000162,0x0003003e,0x00000159,0x00000163,0x0004003d,0x0000000f,
0x00000166,0x00000125,0x0004003d,0x00000006,0x00000167,0x00000159,0x0005008e,0x0000000f,
0x00000168,0x00000166,0x00000167,0x00050051,0x00000006,0x00000169,0x00000168,0x00000000,
0x00050051,0x00000006,0x0000016a,0x00000168,0x00000001,0x00050051,0x00000006,0x0000016b,
0x00000168,0x00000002,0x00070050,0x0000001d,0x0000016c,0x00000169,0x0000016a,0x0000016b,
0x0000001c,0x0003003e,0x00000165,0x0000016c,0x000100fd,0x00010038,0x00050036,0x00000007,
0x0000000d,0x00000000,0x0000000a,0x00030037,0x00000008,0x0000000b,0x00030037,0x00000009,
0x0000000c,0x000200f8,0x0000000e,0x0004003b,0x00000009,0x00000015,0x00000007,0x0004003d,
0x00000007,0x00000016,0x0000000b,0x0004003d,0x00000007,0x00000017,0x0000000b,0x00050094,
0x00000006,0x00000018,0x00000016,0x00000017,0x0003003e,0x00000015,0x00000018,0x0004003d,
0x00000007,0x00000019,0x0000000b,0x0004003d,0x00000006,0x0000001a,0x0000000c,0x0005008e,
0x00000007,0x0000001b,0x00000019,0x0000001a,0x00060041,0x00000025,0x00000026,0x00000020,
0x00000022,0x00000024,0x0004003d,0x00000006,0x00000027,0x00000026,0x0004003d,0x00000006,
0x00000028,0x00000015,0x00050085,0x00000006,0x00000029,0x00000027,0x00000028,0x00050081,
0x00000006,0x0000002a,0x0000001c,0x00000029,0x00060041,0x00000025,0x0000002c,0x00000020,
0x00000022,0x0000002b,0x0004003d,0x00000006,0x0000002d,0x0000002c,0x0004003d,0x00000006,
0x0000002e,0x00000015,0x00050085,0x00000006,0x0000002f,0x0000002d,0x0000002e,0x0004003d,
0x00000006,0x00000030,0x00000015,0x00050085,0x00000006,0x00000031,0x0000002f,0x00000030,
0x00050081,0x00000006,0x00000032,0x0000002a,0x00000031,0x0005008e,0x00000007,0x00000033,
0x0000001b,0x00000032,0x000200fe,0x00000033,0x00010038,0x00050036,0x0000000f,0x00000013,
0x00000000,0x00000010,0x00030037,0x00000008,0x00000011,0x00030037,0x00000009,0x00000012,
0x000200f8,0x00000014,0x0004003b,0x00000009,0x0000003f,0x00000007,0x0004003b,0x00000008,
0x00000042,0x00000007,0x0004003b,0x00000076,0x00000077,0x00000007,0x0004003b,0x00000093,
0x00000094,0x00000007,0x0004003b,0x00000093,0x0000009f,0x00000007,0x0004003b,0x00000009,
0x000000b3,0x00000007,0x0004003b,0x00000093,0x000000bd,0x00000007,0x0004003b,0x00000009,
0x000000c3,0x00000007,0x0004003b,0x00000009,0x000000c6,0x00000007,0x0004003b,0x00000008,
0x000000cb,0x00000007,0x00060041,0x00000025,0x00000038,0x00000020,0x00000036,0x00000037,
0x0004003d,0x00000006,0x00000039,0x00000038,0x000500ba,0x0000003b,0x0000003c,0x00000039,
0x0000003a,0x000300f7,0x0000003e,0x00000000,0x000400fa,0x0000003c,0x0000003d,0x0000003e,
0x000200f8,0x0000003d,0x00060041,0x00000025,0x00000040,0x00000020,0x00000022,0x00000037,
0x0004003d,0x00000006,0x00000041,0x00000040,0x0003003e,0x0000003f,0x00000041,0x00050041,
0x00000009,0x00000043,0x00000011,0x00000024,0x0004003d,0x00000006,0x00000044,0x00000043,
0x0004003d,0x00000006,0x00000045,0x0000003f,0x00060041,0x00000025,0x00000047,0x00000020,
0x00000036,0x00000046,0x0004003d,0x00000006,0x00000048,0x00000047,0x00050085,0x00000006,
0x00000049,0x00000045,0x00000048,0x00050088,0x00000006,0x0000004a,0x00000044,0x00000049,
0x00050041,0x00000009,0x0000004b,0x00000011,0x0000002b,0x0004003d,0x00000006,0x0000004c,
0x0000004b,0x0004003d,0x00000006,0x0000004d,0x0000003f,0x00050088,0x00000006,0x0000004e,
0x0000004c,0x0000004d,0x00050050,0x00000007,0x0000004f,0x0000004a,0x0000004e,0x0005008e,
0x00000007,0x00000050,0x0000004f,0x0000003a,0x00050050,0x00000007,0x00000051,0x0000003a,
0x0000003a,0x00050081,0x00000007,0x00000052,0x00000050,0x00000051,0x0003003e,0x00000042,
0x00000052,0x0004003d,0x00000007,0x00000053,0x00000042,0x000500b8,0x00000056,0x00000057,
0x00000053,0x00000055,0x0004009a,0x0000003b,0x00000058,0x00000057,0x000400a8,0x0000003b,
0x00000059,0x00000058,0x000300f7,0x0000005b,0x00000000,0x000400fa,0x00000059,0x0000005a,
0x0000005b,0x000200f8,0x0000005a,0x0004003d,0x00000007,0x0000005c,0x00000042,0x000500ba,
0x00000056,0x0000005e,0x0000005c,0x0000005d,0x0004009a,0x0000003b,0x0000005f,0x0000005e,
0x000200f9,0x0000005b,0x000200f8,0x0000005b,0x000700f5,0x0000003b,0x00000060,0x00000058,
0x0000003d,0x0000005f,0x0000005a,0x000300f7,0x00000062,0x00000000,0x000400fa,0x00000060,
0x00000061,0x00000062,0x000200f8,0x00000061,0x000200fe,0x00000063,0x000200f8,0x00000062,
0x0004003d,0x00000066,0x00000069,0x00000068,0x00050041,0x00000009,0x0000006a,0x00000042,
0x00000024,0x0004003d,0x00000006,0x0000006b,0x0000006a,0x0004003d,0x00000006,0x0000006c,
0x00000012,0x00050081,0x00000006,0x0000006d,0x0000006b,0x0000006c,0x00050085,0x00000006,
0x0000006e,0x0000006d,0x0000003a,0x00050041,0x00000009,0x0000006f,0x00000042,0x0000002b,
0x0004003d,0x00000006,0x00000070,0x0000006f,0x00050050,0x00000007,0x00000071,0x0000006e,
0x00000070,0x00050057,0x0000001d,0x00000072,0x00000069,0x00000071,0x0008004f,0x0000000f,
0x00000073,0x00000072,0x00000072,0x00000000,0x00000001,0x00000002,0x000200fe,0x00000073,
0x000200f8,0x0000003e,0x00050041,0x00000079,0x0000007a,0x00000020,0x00000078,0x0004003d,
0x0000001d,0x0000007b,0x0000007a,0x0008004f,0x0000000f,0x0000007c,0x0000007b,0x0000007b,
0x00000000,0x00000001,0x00000002,0x00050041,0x00000079,0x0000007e,0x00000020,0x0000007d,
0x0004003d,0x0000001d,0x0000007f,0x0000007e,0x0008004f,0x0000000f,0x00000080,0x0000007f,
0x0000007f,0x00000000,0x00000001,0x00000002,0x00050041,0x00000079,0x00000082,0x00000020,
0x00000081,0x0004003d,0x0000001d,0x00000083,0x00000082,0x0008004f,0x0000000f,0x00000084,
0x00000083,0x00000083,0x00000000,0x00000001,0x00000002,0x00050051,0x00000006,0x00000085,
0x0000007c,0x00000000,0x00050051,0x00000006,0x00000086,0x0000007c,0x00000001,0x00050051,
0x00000006,0x00000087,0x0000007c,0x00000002,0x00050051,0x00000006,0x00000088,0x00000080,
0x00000000,0x00050051,0x00000006,0x00000089,0x00000080,0x00000001,0x00050051,0x00000006,
0x0000008a,0x00000080,0x00000002,0x00050051,0x00000006,0x0000008b,0x00000084,0x00000000,
0x00050051,0x00000006,0x0000008c,0x00000084,0x00000001,0x00050051,0x00000006,0x0000008d,
0x00000084,0x00000002,0x00060050,0x0000000f,0x0000008e,0x00000085,0x00000086,0x00000087,
0x00060050,0x0000000f,0x0000008f,0x00000088,0x00000089,0x0000008a,0x00060050,0x0000000f,
0x00000090,0x0000008b,0x0000008c,0x0000008d,0x00060050,0x00000075,0x00000091,0x0000008e,
0x0000008f,0x00000090,0x00040054,0x00000075,0x00000092,0x00000091,0x0003003e,0x00000077,
0x00000092,0x0004003d,0x00000075,0x00000095,0x00000077,0x00050041,0x00000009,0x00000096,
0x00000011,0x00000024,0x0004003d,0x00000006,0x00000097,0x00000096,0x00050041,0x00000009,
0x00000098,0x00000011,0x0000002b,0x0004003d,0x00000006,0x00000099,0x00000098,0x0004007f,
0x00000006,0x0000009a,0x00000099,0x00060050,0x0000000f,0x0000009c,0x00000097,0x0000009a,
0x0000009b,0x0006000c,0x0000000f,0x0000009d,0x00000001,0x00000045,0x0000009c,0x00050091,
0x0000000f,0x0000009e,0x00000095,0x0000009d,0x0003003e,0x00000094,0x0000009e,0x0004003d,
0x00000075,0x000000a0,0x00000077,0x0004003d,0x00000006,0x000000a1,0x00000012,0x000500b8,
0x0000003b,0x000000a2,0x000000a1,0x0000003a,0x000600a9,0x00000006,0x000000a3,0x000000a2,
0x0000009b,0x0000001c,0x00060041,0x00000025,0x000000a4,0x00000020,0x00000078,0x00000046,
0x0004003d,0x00000006,0x000000a5,0x000000a4,0x00050085,0x00000006,0x000000a6,0x000000a3,
0x000000a5,0x00060050,0x0000000f,0x000000a7,0x000000a6,0x00000054,0x00000054,0x00050091,
0x0000000f,0x000000a8,0x000000a0,0x000000a7,0x0003003e,0x0000009f,0x000000a8,0x00050041,
0x00000009,0x000000a9,0x00000094,0x00000037,0x0004003d,0x00000006,0x000000aa,0x000000a9,
0x000500ba,0x0000003b,0x000000ac,0x000000aa,0x000000ab,0x000300f7,0x000000ae,0x00000000,
0x000400fa,0x000000ac,0x000000ad,0x000000ae,0x000200f8,0x000000ad,0x000200fe,0x000000b1,
0x000200f8,0x000000ae,0x00060041,0x00000025,0x000000b4,0x00000020,0x0000007d,0x00000046,
0x0004003d,0x00000006,0x000000b5,0x000000b4,0x0004007f,0x00000006,0x000000b6,0x000000b5,
0x00050041,0x00000009,0x000000b7,0x0000009f,0x00000037,0x0004003d,0x00000006,0x000000b8,
0x000000b7,0x00050083,0x00000006,0x000000b9,0x000000b6,0x000000b8,0x00050041,0x00000009,
0x000000ba,0x00000094,0x00000037,0x0004003d,0x00000006,0x000000bb,0x000000ba,0x00050088,
0x00000006,0x000000bc,0x000000b9,0x000000bb,0x0003003e,0x000000b3,0x000000bc,0x0004003d,
0x0000000f,0x000000be,0x0000009f,0x0004003d,0x0000000f,0x000000bf,0x00000094,0x0004003d,
0x00000006,0x000000c0,0x000000b3,0x0005008e,0x0000000f,0x000000c1,0x000000bf,0x000000c0,
0x00050081,0x0000000f,0x000000c2,0x000000be,0x000000c1,0x0003003e,0x000000bd,0x000000c2,
0x00060041,0x00000025,0x000000c4,0x00000020,0x00000081,0x00000046,0x0004003d,0x00000006,
0x000000c5,0x000000c4,0x0003003e,0x000000c3,0x000000c5,0x0004003d,0x00000006,0x000000c7,
0x000000c3,0x00060041,0x00000025,0x000000c8,0x00000020,0x00000036,0x0000002b,0x0004003d,
0x00000006,0x000000c9,0x000000c8,0x00050088,0x00000006,0x000000ca,0x000000c7,0x000000c9,
0x0003003e,0x000000c6,0x000000ca,0x00050041,0x00000009,0x000000cc,0x000000bd,0x00000024,
0x0004003d,0x00000006,0x000000cd,0x000000cc,0x0004003d,0x00000006,0x000000ce,0x000000c3,
0x00050088,0x00000006,0x000000cf,0x000000cd,0x000000ce,0x00050041,0x00000009,0x000000d0,
0x000000bd,0x0000002b,0x0004003d,0x00000006,0x000000d1,0x000000d0,0x0004007f,0x00000006,
0x000000d2,0x000000d1,0x0004003d,0x00000006,0x000000d3,0x000000c6,0x00050088,0x00000006,
0x000000d4,0x000000d2,0x000000d3,0x00050050,0x00000007,0x000000d5,0x000000cf,0x000000d4,
0x0005008e,0x00000007,0x000000d6,0x000000d5,0x0000003a,0x00050050,0x00000007,0x000000d7,
0x0000003a,0x0000003a,0x00050081,0x00000007,0x000000d8,0x000000d6,0x000000d7,0x0003003e,
0x000000cb,0x000000d8,0x0004003d,0x00000007,0x000000d9,0x000000cb,0x000500b8,0x00000056,
0x000000da,0x000000d9,0x00000055,0x0004009a,0x0000003b,0x000000db,0x000000da,0x000400a8,
0x0000003b,0x000000dc,0x000000db,0x000300f7,0x000000de,0x00000000,0x000400fa,0x000000dc,
0x000000dd,0x000000de,0x000200f8,0x000000dd,0x0004003d,0x00000007,0x000000df,0x000000cb,
0x000500ba,0x00000056,0x000000e0,0x000000df,0x0000005d,0x0004009a,0x0000003b,0x000000e1,
0x000000e0,0x000200f9,0x000000de,0x000200f8,0x000000de,0x000700f5,0x0000003b,0x000000e2,
0x000000db,0x000000ae,0x000000e1,0x000000dd,0x000300f7,0x000000e4,0x00000000,0x000400fa,
0x000000e2,0x000000e3,0x000000e4,0x000200f8,0x000000e3,0x000200fe,0x000000b1,0x000200f8,
0x000000e4,0x0004003d,0x00000066,0x000000e6,0x00000068,0x0004003d,0x00000007,0x000000e7,
0x000000cb,0x00050057,0x0000001d,0x000000e8,0x000000e6,0x000000e7,0x0008004f,0x0000000f,
0x000000e9,0x000000e8,0x000000e8,0x00000000,0x00000001,0x00000002,0x000200fe,0x000000e9,
0x00010038
0x736e656c,0x00000000,0x00050006,0x0000001e,0x00000005,0x77656976,0x00000000,0x00050006,
0x0000001e,0x00000006,0x72747865,0x00000061,0x00030005,0x00000020,0x00006370,0x00040005,
0x0000003f,0x486e6174,0x00000000,0x00030005,0x00000042,0x00000073,0x00040005,0x00000068,
0x72756f53,0x00006563,0x00030005,0x00000077,0x00000052,0x00030005,0x00000094,0x00000064,
0x00030005,0x0000009f,0x0000006f,0x00030005,0x000000b3,0x00000074,0x00030005,0x000000bd,
0x00746968,0x00040005,0x000000c3,0x666c6168,0x00000057,0x00040005,0x000000c6,0x666c6168,
0x00000048,0x00030005,0x000000cb,0x00000073,0x00040005,0x000000ed,0x6f6e6f6d,0x00000000,
0x00030005,0x000000f2,0x00657965,0x00030005,0x000000f8,0x00007675,0x00040005,0x000000ff,
0x61636f6c,0x0000006c,0x00030005,0x00000110,0x00007863,0x00030005,0x00000124,0x00000070,
0x00040005,0x00000129,0x486e6174,0x00000000,0x00030005,0x0000012c,0x00000074,0x00030005,
0x0000013d,0x00000063,0x00040005,0x00000141,0x61726170,0x0000006d,0x00040005,0x00000143,
0x61726170,0x0000006d,0x00040005,0x00000145,0x61726170,0x0000006d,0x00040005,0x00000146,
0x61726170,0x0000006d,0x00040005,0x0000014b,0x61726170,0x0000006d,0x00040005,0x0000014d,
0x61726170,0x0000006d,0x00040005,0x0000014f,0x61726170,0x0000006d,0x00040005,0x00000150,
0x61726170,0x0000006d,0x00040005,0x00000158,0x61726170,0x0000006d,0x00040005,0x0000015a,
0x61726170,0x0000006d,0x00040005,0x0000015c,0x61726170,0x0000006d,0x00040005,0x0000015d,
0x61726170,0x0000006d,0x00040005,0x00000163,0x61726170,0x0000006d,0x00040005,0x00000165,
0x61726170,0x0000006d,0x00040005,0x00000167,0x61726170,0x0000006d,0x00040005,0x00000168,
0x61726170,0x0000006d,0x00030005,0x0000016b,0x00000065,0x00040005,0x00000171,0x65646166,
0x00000000,0x00050005,0x0000017d,0x4374756f,0x726f6c6f,0x00000000,0x00030047,0x0000001e,
0x00000002,0x00050048,0x0000001e,0x00000000,0x00000023,0x00000000,0x00050048,0x0000001e,
0x00000001,0x00000023,0x00000010,0x00050048,0x0000001e,0x00000002,0x00000023,0x00000020,
0x00050048,0x0000001e,0x00000003,0x00000023,0x00000030,0x00050048,0x0000001e,0x00000004,
0x00000023,0x00000040,0x00050048,0x0000001e,0x00000005,0x00000023,0x00000050,0x00050048,
0x0000001e,0x00000006,0x00000023,0x00000060,0x00040047,0x00000068,0x00000021,0x00000000,
0x00040047,0x00000068,0x00000022,0x00000000,0x00040047,0x000000f8,0x0000001e,0x00000000,
0x00040047,0x0000017d,0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,
0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,0x00000002,
0x00040020,0x00000008,0x00000007,0x00000007,0x00040020,0x00000009,0x00000007,0x00000006,
0x00050021,0x0000000a,0x00000007,0x00000008,0x00000009,0x00040017,0x0000000f,0x00000006,
0x00000003,0x00050021,0x00000010,0x0000000f,0x00000008,0x00000009,0x0004002b,0x00000006,
0x0000001c,0x3f800000,0x00040017,0x0000001d,0x00000006,0x00000004,0x0009001e,0x0000001e,
0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x00040020,
0x0000001f,0x00000009,0x0000001e,0x0004003b,0x0000001f,0x00000020,0x00000009,0x00040015,
0x00000021,0x00000020,0x00000001,0x0004002b,0x00000021,0x00000022,0x00000004,0x00040015,
0x00000023,0x00000020,0x00000000,0x0004002b,0x00000023,0x00000024,0x00000000,0x00040020,
0x00000025,0x00000009,0x00000006,0x0004002b,0x00000023,0x0000002b,0x00000001,0x0004002b,
0x00000021,0x00000036,0x00000005,0x0004002b,0x00000023,0x00000037,0x00000002,0x0004002b,
0x00000006,0x0000003a,0x3f000000,0x00020014,0x0000003b,0x0004002b,0x00000023,0x00000046,
0x00000003,0x0004002b,0x00000006,0x00000054,0x00000000,0x0005002c,0x00000007,0x00000055,
0x00000054,0x00000054,0x00040017,0x00000056,0x0000003b,0x00000002,0x0005002c,0x00000007,
0x0000005d,0x0000001c,0x0000001c,0x0006002c,0x0000000f,0x00000063,0x00000054,0x00000054,
0x00000054,0x00090019,0x00000065,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,
0x00000001,0x00000000,0x0003001b,0x00000066,0x00000065,0x00040020,0x00000067,0x00000000,
0x00000066,0x0004003b,0x00000067,0x00000068,0x00000000,0x00040018,0x00000075,0x0000000f,
0x00000003,0x00040020,0x00000076,0x00000007,0x00000075,0x0004002b,0x00000021,0x00000078,
0x00000001,0x00040020,0x00000079,0x00000009,0x0000001d,0x0004002b,0x00000021,0x0000007d,
0x00000002,0x0004002b,0x00000021,0x00000081,0x00000003,0x00040020,0x00000093,0x00000007,
0x0000000f,0x0004002b,0x00000006,0x0000009b,0xbf800000,0x0004002b,0x00000006,0x000000ab,
0xb8d1b717,0x0004002b,0x00000006,0x000000af,0x3c75c28f,0x0004002b,0x00000006,0x000000b0,
0x3ca3d70a,0x0006002c,0x0000000f,0x000000b1,0x000000af,0x000000af,0x000000b0,0x00040020,
0x000000ec,0x00000007,0x0000003b,0x0004002b,0x00000021,0x000000ee,0x00000006,0x00040020,
0x000000f7,0x00000001,0x00000007,0x0004003b,0x000000f7,0x000000f8,0x00000001,0x00040020,
0x000000f9,0x00000001,0x00000006,0x0004002b,0x00000006,0x00000108,0x40000000,0x0004002b,
0x00000006,0x00000172,0x3cf5c28f,0x00040020,0x0000017c,0x00000003,0x0000001d,0x0004003b,
0x0000017c,0x0000017d,0x00000003,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,
0x000200f8,0x00000005,0x0004003b,0x000000ec,0x000000ed,0x00000007,0x0004003b,0x00000009,
0x000000f2,0x00000007,0x0004003b,0x00000008,0x000000ff,0x00000007,0x0004003b,0x00000008,
0x00000101,0x00000007,0x0004003b,0x00000009,0x00000110,0x00000007,0x0004003b,0x00000009,
0x00000112,0x00000007,0x0004003b,0x00000009,0x00000118,0x00000007,0x0004003b,0x00000008,
0x00000124,0x00000007,0x0004003b,0x00000009,0x00000129,0x00000007,0x0004003b,0x00000008,
0x0000012c,0x00000007,0x0004003b,0x00000093,0x0000013d,0x00000007,0x0004003b,0x00000008,
0x00000141,0x00000007,0x0004003b,0x00000009,0x00000143,0x00000007,0x0004003b,0x00000008,
0x00000145,0x00000007,0x0004003b,0x00000009,0x00000146,0x00000007,0x0004003b,0x00000008,
0x0000014b,0x00000007,0x0004003b,0x00000009,0x0000014d,0x00000007,0x0004003b,0x00000008,
0x0000014f,0x00000007,0x0004003b,0x00000009,0x00000150,0x00000007,0x0004003b,0x00000008,
0x00000158,0x00000007,0x0004003b,0x00000009,0x0000015a,0x00000007,0x0004003b,0x00000008,
0x0000015c,0x00000007,0x0004003b,0x00000009,0x0000015d,0x00000007,0x0004003b,0x00000008,
0x00000163,0x00000007,0x0004003b,0x00000009,0x00000165,0x00000007,0x0004003b,0x00000008,
0x00000167,0x00000007,0x0004003b,0x00000009,0x00000168,0x00000007,0x0004003b,0x00000008,
0x0000016b,0x00000007,0x0004003b,0x00000009,0x00000171,0x00000007,0x00060041,0x00000025,
0x000000ef,0x00000020,0x000000ee,0x00000024,0x0004003d,0x00000006,0x000000f0,0x000000ef,
0x000500ba,0x0000003b,0x000000f1,0x000000f0,0x0000003a,0x0003003e,0x000000ed,0x000000f1,
0x0004003d,0x0000003b,0x000000f3,0x000000ed,0x000400a8,0x0000003b,0x000000f4,0x000000f3,
0x000300f7,0x000000f6,0x00000000,0x000400fa,0x000000f4,0x000000f5,0x000000f6,0x000200f8,
0x000000f5,0x00050041,0x000000f9,0x000000fa,0x000000f8,0x00000024,0x0004003d,0x00000006,
0x000000fb,0x000000fa,0x000500b8,0x0000003b,0x000000fc,0x000000fb,0x0000003a,0x000200f9,
0x000000f6,0x000200f8,0x000000f6,0x000700f5,0x0000003b,0x000000fd,0x000000f3,0x00000005,
0x000000fc,0x000000f5,0x000600a9,0x00000006,0x000000fe,0x000000fd,0x00000054,0x0000001c,
0x0003003e,0x000000f2,0x000000fe,0x0004003d,0x0000003b,0x00000100,0x000000ed,0x000300f7,
0x00000103,0x00000000,0x000400fa,0x00000100,0x00000102,0x00000105,0x000200f8,0x00000102,
0x0004003d,0x00000007,0x00000104,0x000000f8,0x0003003e,0x00000101,0x00000104,0x000200f9,
0x00000103,0x000200f8,0x00000105,0x00050041,0x000000f9,0x00000106,0x000000f8,0x00000024,
0x0004003d,0x00000006,0x00000107,0x00000106,0x00050085,0x00000006,0x00000109,0x00000107,
0x00000108,0x0004003d,0x00000006,0x0000010a,0x000000f2,0x00050083,0x00000006,0x0000010b,
0x00000109,0x0000010a,0x00050041,0x000000f9,0x0000010c,0x000000f8,0x0000002b,0x0004003d,
0x00000006,0x0000010d,0x0000010c,0x00050050,0x00000007,0x0000010e,0x0000010b,0x0000010d,
0x0003003e,0x00000101,0x0000010e,0x000200f9,0x00000103,0x000200f8,0x00000103,0x0004003d,
0x00000007,0x0000010f,0x00000101,0x0003003e,0x000000ff,0x0000010f,0x0004003d,0x0000003b,
0x00000111,0x000000ed,0x000300f7,0x00000114,0x00000000,0x000400fa,0x00000111,0x00000113,
0x00000115,0x000200f8,0x00000113,0x0003003e,0x00000112,0x0000003a,0x000200f9,0x00000114,
0x000200f8,0x00000115,0x0004003d,0x00000006,0x00000116,0x000000f2,0x000500b8,0x0000003b,
0x00000117,0x00000116,0x0000003a,0x000300f7,0x0000011a,0x00000000,0x000400fa,0x00000117,
0x00000119,0x0000011d,0x000200f8,0x00000119,0x00060041,0x00000025,0x0000011b,0x00000020,
0x00000036,0x00000024,0x0004003d,0x00000006,0x0000011c,0x0000011b,0x0003003e,0x00000118,
0x0000011c,0x000200f9,0x0000011a,0x000200f8,0x0000011d,0x00060041,0x00000025,0x0000011e,
0x00000020,0x00000036,0x00000024,0x0004003d,0x00000006,0x0000011f,0x0000011e,0x0004007f,
0x00000006,0x00000120,0x0000011f,0x0003003e,0x00000118,0x00000120,0x000200f9,0x0000011a,
0x000200f8,0x0000011a,0x0004003d,0x00000006,0x00000121,0x00000118,0x00050081,0x00000006,
0x00000122,0x0000003a,0x00000121,0x0003003e,0x00000112,0x00000122,0x000200f9,0x00000114,
0x000200f8,0x00000114,0x0004003d,0x00000006,0x00000123,0x00000112,0x0003003e,0x00000110,
0x00000123,0x0004003d,0x00000007,0x00000125,0x000000ff,0x0004003d,0x00000006,0x00000126,
0x00000110,0x00050050,0x00000007,0x00000127,0x00000126,0x0000003a,0x00050083,0x00000007,
0x00000128,0x00000125,0x00000127,0x0003003e,0x00000124,0x00000128,0x00060041,0x00000025,
0x0000012a,0x00000020,0x00000022,0x00000037,0x0004003d,0x00000006,0x0000012b,0x0000012a,
0x0003003e,0x00000129,0x0000012b,0x00050041,0x00000009,0x0000012d,0x00000124,0x00000024,
0x0004003d,0x00000006,0x0000012e,0x0000012d,0x00060041,0x00000025,0x0000012f,0x00000020,
0x00000036,0x00000046,0x0004003d,0x00000006,0x00000130,0x0000012f,0x00050085,0x00000006,
0x00000131,0x0000012e,0x00000130,0x00050041,0x00000009,0x00000132,0x00000124,0x0000002b,
0x0004003d,0x00000006,0x00000133,0x00000132,0x00050050,0x00000007,0x00000134,0x00000131,
0x00000133,0x0005008e,0x00000007,0x00000135,0x00000134,0x00000108,0x0004003d,0x00000006,
0x00000136,0x00000129,0x0005008e,0x00000007,0x00000137,0x00000135,0x00000136,0x0003003e,
0x0000012c,0x00000137,0x00060041,0x00000025,0x00000138,0x00000020,0x00000022,0x00000046,
0x0004003d,0x00000006,0x00000139,0x00000138,0x000500ba,0x0000003b,0x0000013a,0x00000139,
0x00000054,0x000300f7,0x0000013c,0x00000000,0x000400fa,0x0000013a,0x0000013b,0x00000162,
0x000200f8,0x0000013b,0x00060041,0x00000025,0x0000013e,0x00000020,0x00000022,0x00000046,
0x0004003d,0x00000006,0x0000013f,0x0000013e,0x00050083,0x00000006,0x00000140,0x0000001c,
0x0000013f,0x0004003d,0x00000007,0x00000142,0x0000012c,0x0003003e,0x00000141,0x00000142,
0x0003003e,0x00000143,0x00000140,0x00060039,0x00000007,0x00000144,0x0000000d,0x00000141,
0x00000143,0x0003003e,0x00000145,0x00000144,0x0004003d,0x00000006,0x00000147,0x000000f2,
0x0003003e,0x00000146,0x00000147,0x00060039,0x0000000f,0x00000148,0x00000013,0x00000145,
0x00000146,0x00050051,0x00000006,0x00000149,0x00000148,0x00000000,0x00050041,0x00000009,
0x0000014a,0x0000013d,0x00000024,0x0003003e,0x0000014a,0x00000149,0x0004003d,0x00000007,
0x0000014c,0x0000012c,0x0003003e,0x0000014b,0x0000014c,0x0003003e,0x0000014d,0x0000001c,
0x00060039,0x00000007,0x0000014e,0x0000000d,0x0000014b,0x0000014d,0x0003003e,0x0000014f,
0x0000014e,0x0004003d,0x00000006,0x00000151,0x000000f2,0x0003003e,0x00000150,0x00000151,
0x00060039,0x0000000f,0x00000152,0x00000013,0x0000014f,0x00000150,0x00050051,0x00000006,
0x00000153,0x00000152,0x00000001,0x00050041,0x00000009,0x00000154,0x0000013d,0x0000002b,
0x0003003e,0x00000154,0x00000153,0x00060041,0x00000025,0x00000155,0x00000020,0x00000022,
0x00000046,0x0004003d,0x00000006,0x00000156,0x00000155,0x00050081,0x00000006,0x00000157,
0x0000001c,0x00000156,0x0004003d,0x00000007,0x00000159,0x0000012c,0x0003003e,0x00000158,
0x00000159,0x0003003e,0x0000015a,0x00000157,0x00060039,0x00000007,0x0000015b,0x0000000d,
0x00000158,0x0000015a,0x0003003e,0x0000015c,0x0000015b,0x0004003d,0x00000006,0x0000015e,
0x000000f2,0x0003003e,0x0000015d,0x0000015e,0x00060039,0x0000000f,0x0000015f,0x00000013,
0x0000015c,0x0000015d,0x00050051,0x00000006,0x00000160,0x0000015f,0x00000002,0x00050041,
0x00000009,0x00000161,0x0000013d,0x00000037,0x0003003e,0x00000161,0x00000160,0x000200f9,
0x0000013c,0x000200f8,0x00000162,0x0004003d,0x00000007,0x00000164,0x0000012c,0x0003003e,
0x00000163,0x00000164,0x0003003e,0x00000165,0x0000001c,0x00060039,0x00000007,0x00000166,
0x0000000d,0x00000163,0x00000165,0x0003003e,0x00000167,0x00000166,0x0004003d,0x00000006,
0x00000169,0x000000f2,0x0003003e,0x00000168,0x00000169,0x00060039,0x0000000f,0x0000016a,
0x00000013,0x00000167,0x00000168,0x0003003e,0x0000013d,0x0000016a,0x000200f9,0x0000013c,
0x000200f8,0x0000013c,0x0004003d,0x00000007,0x0000016c,0x000000ff,0x0004003d,0x00000007,
0x0000016d,0x000000ff,0x00050050,0x00000007,0x0000016e,0x0000001c,0x0000001c,0x00050083,
0x00000007,0x0000016f,0x0000016e,0x0000016d,0x0007000c,0x00000007,0x00000170,0x00000001,
0x00000025,0x0000016c,0x0000016f,0x0003003e,0x0000016b,0x00000170,0x00050041,0x00000009,
0x00000173,0x0000016b,0x00000024,0x0004003d,0x00000006,0x00000174,0x00000173,0x00060041,
0x00000025,0x00000175,0x00000020,0x00000036,0x00000046,0x0004003d,0x00000006,0x00000176,
0x00000175,0x00050085,0x00000006,0x00000177,0x00000174,0x00000176,0x00050041,0x00000009,
0x00000178,0x0000016b,0x0000002b,0x0004003d,0x00000006,0x00000179,0x00000178,0x0007000c,
0x00000006,0x0000017a,0x00000001,0x00000025,0x00000177,0x00000179,0x0008000c,0x00000006,
0x0000017b,0x00000001,0x00000031,0x00000054,0x00000172,0x0000017a,0x0003003e,0x00000171,
0x0000017b,0x0004003d,0x0000000f,0x0000017e,0x0000013d,0x0004003d,0x00000006,0x0000017f,
0x00000171,0x0005008e,0x0000000f,0x00000180,0x0000017e,0x0000017f,0x00050051,0x00000006,
0x00000181,0x00000180,0x00000000,0x00050051,0x00000006,0x00000182,0x00000180,0x00000001,
0x00050051,0x00000006,0x00000183,0x00000180,0x00000002,0x00070050,0x0000001d,0x00000184,
0x00000181,0x00000182,0x00000183,0x0000001c,0x0003003e,0x0000017d,0x00000184,0x000100fd,
0x00010038,0x00050036,0x00000007,0x0000000d,0x00000000,0x0000000a,0x00030037,0x00000008,
0x0000000b,0x00030037,0x00000009,0x0000000c,0x000200f8,0x0000000e,0x0004003b,0x00000009,
0x00000015,0x00000007,0x0004003d,0x00000007,0x00000016,0x0000000b,0x0004003d,0x00000007,
0x00000017,0x0000000b,0x00050094,0x00000006,0x00000018,0x00000016,0x00000017,0x0003003e,
0x00000015,0x00000018,0x0004003d,0x00000007,0x00000019,0x0000000b,0x0004003d,0x00000006,
0x0000001a,0x0000000c,0x0005008e,0x00000007,0x0000001b,0x00000019,0x0000001a,0x00060041,
0x00000025,0x00000026,0x00000020,0x00000022,0x00000024,0x0004003d,0x00000006,0x00000027,
0x00000026,0x0004003d,0x00000006,0x00000028,0x00000015,0x00050085,0x00000006,0x00000029,
0x00000027,0x00000028,0x00050081,0x00000006,0x0000002a,0x0000001c,0x00000029,0x00060041,
0x00000025,0x0000002c,0x00000020,0x00000022,0x0000002b,0x0004003d,0x00000006,0x0000002d,
0x0000002c,0x0004003d,0x00000006,0x0000002e,0x00000015,0x00050085,0x00000006,0x0000002f,
0x0000002d,0x0000002e,0x0004003d,0x00000006,0x00000030,0x00000015,0x00050085,0x00000006,
0x00000031,0x0000002f,0x00000030,0x00050081,0x00000006,0x00000032,0x0000002a,0x00000031,
0x0005008e,0x00000007,0x00000033,0x0000001b,0x00000032,0x000200fe,0x00000033,0x00010038,
0x00050036,0x0000000f,0x00000013,0x00000000,0x00000010,0x00030037,0x00000008,0x00000011,
0x00030037,0x00000009,0x00000012,0x000200f8,0x00000014,0x0004003b,0x00000009,0x0000003f,
0x00000007,0x0004003b,0x00000008,0x00000042,0x00000007,0x0004003b,0x00000076,0x00000077,
0x00000007,0x0004003b,0x00000093,0x00000094,0x00000007,0x0004003b,0x00000093,0x0000009f,
0x00000007,0x0004003b,0x00000009,0x000000b3,0x00000007,0x0004003b,0x00000093,0x000000bd,
0x00000007,0x0004003b,0x00000009,0x000000c3,0x00000007,0x0004003b,0x00000009,0x000000c6,
0x00000007,0x0004003b,0x00000008,0x000000cb,0x00000007,0x00060041,0x00000025,0x00000038,
0x00000020,0x00000036,0x00000037,0x0004003d,0x00000006,0x00000039,0x00000038,0x000500ba,
0x0000003b,0x0000003c,0x00000039,0x0000003a,0x000300f7,0x0000003e,0x00000000,0x000400fa,
0x0000003c,0x0000003d,0x0000003e,0x000200f8,0x0000003d,0x00060041,0x00000025,0x00000040,
0x00000020,0x00000022,0x00000037,0x0004003d,0x00000006,0x00000041,0x00000040,0x0003003e,
0x0000003f,0x00000041,0x00050041,0x00000009,0x00000043,0x00000011,0x00000024,0x0004003d,
0x00000006,0x00000044,0x00000043,0x0004003d,0x00000006,0x00000045,0x0000003f,0x00060041,
0x00000025,0x00000047,0x00000020,0x00000036,0x00000046,0x0004003d,0x00000006,0x00000048,
0x00000047,0x00050085,0x00000006,0x00000049,0x00000045,0x00000048,0x00050088,0x00000006,
0x0000004a,0x00000044,0x00000049,0x00050041,0x00000009,0x0000004b,0x00000011,0x0000002b,
0x0004003d,0x00000006,0x0000004c,0x0000004b,0x0004003d,0x00000006,0x0000004d,0x0000003f,
0x00050088,0x00000006,0x0000004e,0x0000004c,0x0000004d,0x00050050,0x00000007,0x0000004f,
0x0000004a,0x0000004e,0x0005008e,0x00000007,0x00000050,0x0000004f,0x0000003a,0x00050050,
0x00000007,0x00000051,0x0000003a,0x0000003a,0x00050081,0x00000007,0x00000052,0x00000050,
0x00000051,0x0003003e,0x00000042,0x00000052,0x0004003d,0x00000007,0x00000053,0x00000042,
0x000500b8,0x00000056,0x00000057,0x00000053,0x00000055,0x0004009a,0x0000003b,0x00000058,
0x00000057,0x000400a8,0x0000003b,0x00000059,0x00000058,0x000300f7,0x0000005b,0x00000000,
0x000400fa,0x00000059,0x0000005a,0x0000005b,0x000200f8,0x0000005a,0x0004003d,0x00000007,
0x0000005c,0x00000042,0x000500ba,0x00000056,0x0000005e,0x0000005c,0x0000005d,0x0004009a,
0x0000003b,0x0000005f,0x0000005e,0x000200f9,0x0000005b,0x000200f8,0x0000005b,0x000700f5,
0x0000003b,0x00000060,0x00000058,0x0000003d,0x0000005f,0x0000005a,0x000300f7,0x00000062,
0x00000000,0x000400fa,0x00000060,0x00000061,0x00000062,0x000200f8,0x00000061,0x000200fe,
0x00000063,0x000200f8,0x00000062,0x0004003d,0x00000066,0x00000069,0x00000068,0x00050041,
0x00000009,0x0000006a,0x00000042,0x00000024,0x0004003d,0x00000006,0x0000006b,0x0000006a,
0x0004003d,0x00000006,0x0000006c,0x00000012,0x00050081,0x00000006,0x0000006d,0x0000006b,
0x0000006c,0x00050085,0x00000006,0x0000006e,0x0000006d,0x0000003a,0x00050041,0x00000009,
0x0000006f,0x00000042,0x0000002b,0x0004003d,0x00000006,0x00000070,0x0000006f,0x00050050,
0x00000007,0x00000071,0x0000006e,0x00000070,0x00050057,0x0000001d,0x00000072,0x00000069,
0x00000071,0x0008004f,0x0000000f,0x00000073,0x00000072,0x00000072,0x00000000,0x00000001,
0x00000002,0x000200fe,0x00000073,0x000200f8,0x0000003e,0x00050041,0x00000079,0x0000007a,
0x00000020,0x00000078,0x0004003d,0x0000001d,0x0000007b,0x0000007a,0x0008004f,0x0000000f,
0x0000007c,0x0000007b,0x0000007b,0x00000000,0x00000001,0x00000002,0x00050041,0x00000079,
0x0000007e,0x00000020,0x0000007d,0x0004003d,0x0000001d,0x0000007f,0x0000007e,0x0008004f,
0x0000000f,0x00000080,0x0000007f,0x0000007f,0x00000000,0x00000001,0x00000002,0x00050041,
0x00000079,0x00000082,0x00000020,0x00000081,0x0004003d,0x0000001d,0x00000083,0x00000082,
0x0008004f,0x0000000f,0x00000084,0x00000083,0x00000083,0x00000000,0x00000001,0x00000002,
0x00050051,0x00000006,0x00000085,0x0000007c,0x00000000,0x00050051,0x00000006,0x00000086,
0x0000007c,0x00000001,0x00050051,0x00000006,0x00000087,0x0000007c,0x00000002,0x00050051,
0x00000006,0x00000088,0x00000080,0x00000000,0x00050051,0x00000006,0x00000089,0x00000080,
0x00000001,0x00050051,0x00000006,0x0000008a,0x00000080,0x00000002,0x00050051,0x00000006,
0x0000008b,0x00000084,0x00000000,0x00050051,0x00000006,0x0000008c,0x00000084,0x00000001,
0x00050051,0x00000006,0x0000008d,0x00000084,0x00000002,0x00060050,0x0000000f,0x0000008e,
0x00000085,0x00000086,0x00000087,0x00060050,0x0000000f,0x0000008f,0x00000088,0x00000089,
0x0000008a,0x00060050,0x0000000f,0x00000090,0x0000008b,0x0000008c,0x0000008d,0x00060050,
0x00000075,0x00000091,0x0000008e,0x0000008f,0x00000090,0x00040054,0x00000075,0x00000092,
0x00000091,0x0003003e,0x00000077,0x00000092,0x0004003d,0x00000075,0x00000095,0x00000077,
0x00050041,0x00000009,0x00000096,0x00000011,0x00000024,0x0004003d,0x00000006,0x00000097,
0x00000096,0x00050041,0x00000009,0x00000098,0x00000011,0x0000002b,0x0004003d,0x00000006,
0x00000099,0x00000098,0x0004007f,0x00000006,0x0000009a,0x00000099,0x00060050,0x0000000f,
0x0000009c,0x00000097,0x0000009a,0x0000009b,0x0006000c,0x0000000f,0x0000009d,0x00000001,
0x00000045,0x0000009c,0x00050091,0x0000000f,0x0000009e,0x00000095,0x0000009d,0x0003003e,
0x00000094,0x0000009e,0x0004003d,0x00000075,0x000000a0,0x00000077,0x0004003d,0x00000006,
0x000000a1,0x00000012,0x000500b8,0x0000003b,0x000000a2,0x000000a1,0x0000003a,0x000600a9,
0x00000006,0x000000a3,0x000000a2,0x0000009b,0x0000001c,0x00060041,0x00000025,0x000000a4,
0x00000020,0x00000078,0x00000046,0x0004003d,0x00000006,0x000000a5,0x000000a4,0x00050085,
0x00000006,0x000000a6,0x000000a3,0x000000a5,0x00060050,0x0000000f,0x000000a7,0x000000a6,
0x00000054,0x00000054,0x00050091,0x0000000f,0x000000a8,0x000000a0,0x000000a7,0x0003003e,
0x0000009f,0x000000a8,0x00050041,0x00000009,0x000000a9,0x00000094,0x00000037,0x0004003d,
0x00000006,0x000000aa,0x000000a9,0x000500ba,0x0000003b,0x000000ac,0x000000aa,0x000000ab,
0x000300f7,0x000000ae,0x00000000,0x000400fa,0x000000ac,0x000000ad,0x000000ae,0x000200f8,
0x000000ad,0x000200fe,0x000000b1,0x000200f8,0x000000ae,0x00060041,0x00000025,0x000000b4,
0x00000020,0x0000007d,0x00000046,0x0004003d,0x00000006,0x000000b5,0x000000b4,0x0004007f,
0x00000006,0x000000b6,0x000000b5,0x00050041,0x00000009,0x000000b7,0x0000009f,0x00000037,
0x0004003d,0x00000006,0x000000b8,0x000000b7,0x00050083,0x00000006,0x000000b9,0x000000b6,
0x000000b8,0x00050041,0x00000009,0x000000ba,0x00000094,0x00000037,0x0004003d,0x00000006,
0x000000bb,0x000000ba,0x00050088,0x00000006,0x000000bc,0x000000b9,0x000000bb,0x0003003e,
0x000000b3,0x000000bc,0x0004003d,0x0000000f,0x000000be,0x0000009f,0x0004003d,0x0000000f,
0x000000bf,0x00000094,0x0004003d,0x00000006,0x000000c0,0x000000b3,0x0005008e,0x0000000f,
0x000000c1,0x000000bf,0x000000c0,0x00050081,0x0000000f,0x000000c2,0x000000be,0x000000c1,
0x0003003e,0x000000bd,0x000000c2,0x00060041,0x00000025,0x000000c4,0x00000020,0x00000081,
0x00000046,0x0004003d,0x00000006,0x000000c5,0x000000c4,0x0003003e,0x000000c3,0x000000c5,
0x0004003d,0x00000006,0x000000c7,0x000000c3,0x00060041,0x00000025,0x000000c8,0x00000020,
0x00000036,0x0000002b,0x0004003d,0x00000006,0x000000c9,0x000000c8,0x00050088,0x00000006,
0x000000ca,0x000000c7,0x000000c9,0x0003003e,0x000000c6,0x000000ca,0x00050041,0x00000009,
0x000000cc,0x000000bd,0x00000024,0x0004003d,0x00000006,0x000000cd,0x000000cc,0x0004003d,
0x00000006,0x000000ce,0x000000c3,0x00050088,0x00000006,0x000000cf,0x000000cd,0x000000ce,
0x00050041,0x00000009,0x000000d0,0x000000bd,0x0000002b,0x0004003d,0x00000006,0x000000d1,
0x000000d0,0x0004007f,0x00000006,0x000000d2,0x000000d1,0x0004003d,0x00000006,0x000000d3,
0x000000c6,0x00050088,0x00000006,0x000000d4,0x000000d2,0x000000d3,0x00050050,0x00000007,
0x000000d5,0x000000cf,0x000000d4,0x0005008e,0x00000007,0x000000d6,0x000000d5,0x0000003a,
0x00050050,0x00000007,0x000000d7,0x0000003a,0x0000003a,0x00050081,0x00000007,0x000000d8,
0x000000d6,0x000000d7,0x0003003e,0x000000cb,0x000000d8,0x0004003d,0x00000007,0x000000d9,
0x000000cb,0x000500b8,0x00000056,0x000000da,0x000000d9,0x00000055,0x0004009a,0x0000003b,
0x000000db,0x000000da,0x000400a8,0x0000003b,0x000000dc,0x000000db,0x000300f7,0x000000de,
0x00000000,0x000400fa,0x000000dc,0x000000dd,0x000000de,0x000200f8,0x000000dd,0x0004003d,
0x00000007,0x000000df,0x000000cb,0x000500ba,0x00000056,0x000000e0,0x000000df,0x0000005d,
0x0004009a,0x0000003b,0x000000e1,0x000000e0,0x000200f9,0x000000de,0x000200f8,0x000000de,
0x000700f5,0x0000003b,0x000000e2,0x000000db,0x000000ae,0x000000e1,0x000000dd,0x000300f7,
0x000000e4,0x00000000,0x000400fa,0x000000e2,0x000000e3,0x000000e4,0x000200f8,0x000000e3,
0x000200fe,0x000000b1,0x000200f8,0x000000e4,0x0004003d,0x00000066,0x000000e6,0x00000068,
0x0004003d,0x00000007,0x000000e7,0x000000cb,0x00050057,0x0000001d,0x000000e8,0x000000e6,
0x000000e7,0x0008004f,0x0000000f,0x000000e9,0x000000e8,0x000000e8,0x00000000,0x00000001,
0x00000002,0x000200fe,0x000000e9,0x00010038
};
@@ -22,6 +22,7 @@
enum {
DROIDDECK_HEADPOSE_LIVE = 1u << 0, /* a rotation sensor is feeding it (else identity) */
DROIDDECK_HEADPOSE_ENABLED = 1u << 1, /* Cardboard mode is on */
DROIDDECK_HEADPOSE_MONO = 1u << 2,
};
struct droiddeck_headpose {
@@ -37,7 +38,8 @@ struct droiddeck_headpose {
float tan_half_fov;
float eye_aspect;
uint32_t eye_w, eye_h; /* one eye's viewport on the panel, pixels (0 = not drawn yet) */
uint32_t reserved[12];
float display_hz;
uint32_t reserved[11];
};
#ifdef __cplusplus
@@ -467,10 +467,11 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScreenEffects(JNI
/* Cardboard mode - WaylandCompositor.nativeSetCardboard. */
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboard(JNIEnv *env, jclass clazz, jboolean enabled,
jint source, jfloat ipd, jfloat dist, jfloat width, jfloat k1, jfloat k2, jfloat fov, jfloat inset, jfloat chroma) {
jint source, jfloat ipd, jfloat dist, jfloat width, jfloat k1, jfloat k2, jfloat fov, jfloat inset, jfloat chroma,
jboolean mono, jfloat hz) {
struct cardboard_params p = {.enabled = enabled ? 1 : 0, .source = source, .ipd_m = ipd, .screen_dist_m = dist,
.screen_width_m = width, .k1 = k1, .k2 = k2, .fov_deg = fov, .lens_inset = inset,
.chroma = chroma};
.chroma = chroma, .mono = mono ? 1 : 0, .display_hz = hz};
vkp_cardboard_set(&p);
}
@@ -26,6 +26,7 @@ class App : Application() {
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
com.droiddeck.launcher.update.AppUpdates.init(this)
GpuClockPin.clearLeftover(this)
com.droiddeck.launcher.glasses.Glasses.init(this)
BwrapSpawner.start(this)
com.droiddeck.launcher.frontend.GameFileSync.start(this)
// The game stores the Stores section can talk to; the native engine loads only when one needs it.
@@ -52,6 +52,11 @@ import com.droiddeck.launcher.ui.SessionAchievements
import com.droiddeck.launcher.gpu.Cardboard
import com.droiddeck.launcher.gpu.FrameGen
import com.droiddeck.launcher.gpu.HeadTracker
import com.droiddeck.launcher.glasses.Glasses
import com.droiddeck.launcher.glasses.GlassesScreen
import com.droiddeck.launcher.glasses.GlassesSettings
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import com.droiddeck.launcher.gpu.SteamVr
import com.droiddeck.launcher.gpu.ScreenEffectLooks
import com.droiddeck.launcher.gpu.ScreenEffects
@@ -308,6 +313,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private var steamVrSwitchedPicture by mutableStateOf(false)
/** SteamVR has been seen running since the switch: only then does "not running" mean it stopped. */
private var steamVrSeenRunning = false
private var activeCardboard = Cardboard.OFF
private var glasses by mutableStateOf(Glasses.state.value)
private var glassesSettings by mutableStateOf(Glasses.settings.value)
private var glassesScreen: GlassesScreen? = null
private var glassesOutputActive by mutableStateOf(false)
private var glasses3dForVr = false
private var glassesSlot by mutableStateOf(0)
private var glassesKey: List<Any?> = emptyList()
private val glassesScope = kotlinx.coroutines.MainScope()
private val hasRotationSensor by lazy {
(getSystemService(SENSOR_SERVICE) as android.hardware.SensorManager).let {
it.getDefaultSensor(android.hardware.Sensor.TYPE_GAME_ROTATION_VECTOR)
@@ -510,6 +524,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
sessionOverlay = createSessionOverlay()
root.addView(sessionOverlay)
setContentView(root)
glassesScope.launch { Glasses.state.collect { glasses = it; onGlassesChanged() } }
glassesScope.launch { Glasses.settings.collect { glassesSettings = it; onGlassesChanged() } }
pip = com.droiddeck.launcher.session.SessionPipController(this, ::pipBounds, ::showPipUi)
surfaceView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> pip.refresh() }
pip.refresh()
@@ -565,6 +581,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
CursorOverlay(cursorPos, cursorVisible, resources.displayMetrics.density,
cursorImage, cursorHotX, cursorHotY, cursorImageScale)
if (hud.text.isNotEmpty()) HudText(hud.text)
if (glassesOutputActive && !loading.visible) com.droiddeck.launcher.ui.GlassesPlayingHint(glasses.model)
if (glasses.present && !drawerOpen && !pcKeyboardOpen && !loading.visible) com.droiddeck.launcher.ui.GlassesCornerButton(glassesSlot,
controllerSettings?.tint ?: android.graphics.Color.rgb(90, 160, 255), controllerSettings?.opacity ?: 100) {
drawerPage = com.droiddeck.launcher.ui.SessionDrawerPage.GLASSES
drawerOpen = true
}
if (loading.visible) LoadingOverlay(
loading.step, loading.percent, loading.elapsed, loading.hint, loading.ended,
topic = loading.topic, readable = loading.readable,
@@ -596,8 +618,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
// SteamVR's state for the Cardboard group, refreshed while the drawer shows it.
// Watched for as long as Cardboard mode is on (or the picture is switched for it), drawer open or
// not: however SteamVR is started or quit, the picture follows it.
LaunchedEffect(cardboard.enabled, steamVrSwitchedPicture) {
while (cardboard.enabled || steamVrSwitchedPicture) {
val glassesVr = glassesOutputActive && glasses.canControl
LaunchedEffect(cardboard.enabled, steamVrSwitchedPicture, glassesVr) {
while (cardboard.enabled || steamVrSwitchedPicture || glassesVr) {
val now = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
SteamVr.status(this@SessionActivity)
}
@@ -634,6 +657,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
cardboardSensor = hasRotationSensor,
onCardboard = { c -> SessionPrefs.setCardboard(this@SessionActivity, c); applyCardboard(c) },
onCardboardRecenter = { headTracker?.recenter() },
glassesPresent = glasses.present,
glassesOutput = glassesOutputActive,
onGlassesRecenter = { headTracker?.recenter() },
steamVr = steamVrStatus,
onSteamVr = { action -> steamVrAction(action) },
textureAnisotropy = if (drawerGameProfileId == null) textureAnisotropy else drawerTextureAnisotropy,
@@ -1337,6 +1363,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
override fun surfaceCreated(holder: SurfaceHolder) {
if (holder !== outputHolder()) return
if (installingRuntime || SessionState.stopRequested) return
if (!SessionState.running) {
SessionEvents.transition(SessionPhase.STARTING_COMPOSITOR, "compositor.starting")
@@ -1361,7 +1388,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
val live = if (SessionState.followScreen && !isInPictureInPictureMode)
com.droiddeck.launcher.session.SessionDisplay.followSize(holder.surfaceFrame.width(), holder.surfaceFrame.height())
else null
val size = live ?: if (SessionState.running) previous else outputSize()
val size = live ?: if (SessionState.running) previous else glassesOutputSize() ?: outputSize()
SessionState.outputSize = size
if (!SessionState.running) SessionState.refreshHz = refreshHz()
// How far the panel enlarges the session, for texture sharpness "Auto" (TextureFiltering):
@@ -1378,7 +1405,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
WaylandCompositor.nativeSetUpscaler(SessionPrefs.upscaler(this))
WaylandCompositor.nativeSetUpscaleSharpness(SessionPrefs.upscaleSharpness(this))
SessionPrefs.screenEffects(this).push(SessionPrefs.upscaler(this))
panelSizeMm().let { (w, h) -> SessionPrefs.cardboard(this).push(w, h) }
activeCardboard = effectiveCardboard(SessionPrefs.cardboard(this))
pushCardboard(activeCardboard)
WaylandCompositor.nativeSetCardboardPoseFile(File(filesDir, ".wayland-rt/droiddeck-headpose").path)
// The session's folder, claimed here because the compositor starts before the service and
// opens its log once. The compositor reads the path from its environment; setting it after
@@ -1448,6 +1476,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private var surfaceH = 0
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if (holder !== outputHolder()) return
val resized = surfaceW != 0 && (width != surfaceW || height != surfaceH)
surfaceW = width
surfaceH = height
@@ -1542,6 +1571,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
* active; it is the rate gamescope gives Steam, and the QAM's Frame Limit divides it.
*/
private fun refreshHz(): Float {
if (glassesScreen != null) glasses.hz.takeIf { it > 1f }?.let { return it }
val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay
val wanted = window.attributes.preferredDisplayModeId
val hz = display?.supportedModes?.firstOrNull { wanted != 0 && it.modeId == wanted }?.refreshRate
@@ -2005,6 +2035,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
if (!::displayManager.isInitialized) return
val primaryId = display?.displayId ?: windowManager.defaultDisplay.displayId
val candidates = SecondScreenDisplays.available(displayManager, primaryId)
.filter { glassesScreen == null || it.id != glasses.displayId }
val oldIds = secondScreenDisplays.map { it.id }.toSet()
secondScreenDisplays = candidates
val newIds = candidates.map { it.id }.toSet()
@@ -2357,6 +2388,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
private fun updateOnScreenControls() {
updateOnScreenPad()
val c = onScreenControls
glassesSlot = when {
c == null || c.visibility != View.VISIBLE -> 0
c.isButtonsOnly -> -1
controllerSettings?.keyboardButton != false -> 2
else -> 1
}
}
private fun updateOnScreenPad() {
val forced = File(Environment.getExternalStorageDirectory(), "Download/droiddeck-osc")
.takeIf { it.isFile }
?.let { FileUtils.readString(it)?.trim()?.lowercase() }
@@ -2487,6 +2529,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
// The panel may have changed while the session was a thumbnail (folded or unfolded in PiP).
if (!inPip && surfaceW > 0) followWindow(surfaceW, surfaceH)
applyUnfoldedControls()
updateGlassesScreen()
}
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
@@ -2506,6 +2549,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
refreshSecondScreenDisplays()
if (secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode)
SessionService.setActivityVisible(this, true)
Glasses.rescan()
updateGlassesScreen()
}
override fun onStop() {
@@ -2515,6 +2560,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
// Closed while the session is out of sight (sleep, a closed lid, another app), and kept:
// onStart puts it back.
closeSecondScreen(reset = false)
updateGlassesScreen()
SessionService.setActivityVisible(this, false)
super.onStop()
}
@@ -2591,6 +2637,22 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
/** SteamVR for Cardboard mode: install, start or stop it in the guest. Starting switches the
* picture to side-by-side, which is what SteamVR draws. */
private fun steamVrAction(action: String) {
if (glassesOutputActive) {
if (action == "start" && glassesSettings.auto3d && glasses.can3d && !glasses.sbs) {
glasses3dForVr = true
Glasses.set3d(true)
}
if (action == "install") steamVrStatus = SteamVr.Status.INSTALLING
Thread({
val ok = SteamVr.run(this, action)
val now = SteamVr.status(this)
uiHandler.post {
onSteamVrStatus(now)
if (!ok) Toast.makeText(this, getString(R.string.cardboard_steamvr_failed), Toast.LENGTH_LONG).show()
}
}, "steamvr-$action").start()
return
}
if (action == "start") {
switchPictureForSteamVr()
steamVrSeenRunning = false
@@ -2621,7 +2683,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
private fun onSteamVrStatus(now: SteamVr.Status) {
val was = steamVrStatus
steamVrStatus = now
if (glassesOutputActive) {
onGlassesSteamVr(now)
if ((was == SteamVr.Status.RUNNING) != (now == SteamVr.Status.RUNNING)) applyCardboard(cardboard)
return
}
// Started some other way than the drawer (a script, a VR game): the picture follows it all the same.
if (now == SteamVr.Status.RUNNING && cardboard.enabled && !steamVrSwitchedPicture &&
cardboard.source != Cardboard.SOURCE_SBS) {
@@ -2647,25 +2715,124 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
/** Cardboard mode: hand the settings to the compositor and run the head tracker while it needs one. */
private fun applyCardboard(c: Cardboard) {
val was = cardboard
val was = activeCardboard
cardboard = c
if (CompositorHost.isStarted) panelSizeMm().let { (w, h) -> c.push(w, h) }
if (c.enabled && !was.enabled) headTracker?.recenter()
val e = effectiveCardboard(c)
activeCardboard = e
if (CompositorHost.isStarted) pushCardboard(e)
if (e.enabled && !was.enabled) headTracker?.recenter()
updateHeadTracker()
}
private fun pushCardboard(c: Cardboard) {
val fov = if (c.viewer == Cardboard.Viewer.VITURE.id) glasses.verticalFovDeg else 0f
panelSizeMm().let { (w, h) -> c.push(w, h, fov, refreshHz()) }
}
private fun effectiveCardboard(c: Cardboard): Cardboard {
if (glassesScreen == null) return c
val s = glassesSettings
val source = when (s.picture3d) {
GlassesSettings.PICTURE_SBS -> Cardboard.SOURCE_SBS
GlassesSettings.PICTURE_SCREEN -> Cardboard.SOURCE_SCREEN
else -> when {
steamVrStatus == SteamVr.Status.RUNNING -> Cardboard.SOURCE_SBS
glasses.sbs -> Cardboard.SOURCE_SCREEN
else -> return Cardboard.OFF
}
}
return Cardboard(
enabled = true, source = source,
viewer = Cardboard.Viewer.VITURE.id, ipdMm = c.ipdMm,
screenWidthDm = s.screenWidthDm, screenDistanceDm = s.screenDistanceDm,
headTracking = s.headTracking && glasses.canControl,
mono = !glasses.sbs,
)
}
private fun updateHeadTracker() {
// Side-by-side sources track too: the compositor leaves their picture alone, but whoever renders
// it (the SteamVR driver, tools/vr) reads the pose from the shared file.
val want = resumed && cardboard.enabled && cardboard.headTracking
val onGlasses = glassesScreen != null
val c = activeCardboard
val want = (resumed || (onGlasses && started)) && c.enabled && c.headTracking
val current = headTracker
if (current != null && (!want || current.usesGlasses != onGlasses)) {
current.stop()
headTracker = null
}
if (want) {
val t = headTracker ?: HeadTracker(this) {
val t = headTracker ?: HeadTracker(this, glasses = onGlasses) {
@Suppress("DEPRECATION")
(if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay)?.rotation ?: android.view.Surface.ROTATION_0
}.also { headTracker = it }
t.start()
} else {
headTracker?.stop()
}
}
private fun outputHolder(): SurfaceHolder = glassesScreen?.holder ?: surfaceView.holder
private fun glassesOutputSize(): Pair<Int, Int>? {
if (glassesScreen == null || glasses.width <= 0 || glasses.height <= 0) return null
val panel = (if (glasses.sbs) glasses.width / 2 else glasses.width) to glasses.height
val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM)
return com.droiddeck.launcher.session.SessionDisplay.resolveChoice(panel, SessionPrefs.resolutionChoice(this, mode, panel))
}
private fun onGlassesChanged() {
val g = glasses
val s = glassesSettings
val key = listOf(g.usable, g.displayId, g.sbs, g.canControl, g.width, g.height, g.hz, s)
if (key == glassesKey) return
glassesKey = key
updateGlassesScreen()
applyCardboard(cardboard)
}
private fun updateGlassesScreen() {
if (!::surfaceView.isInitialized) return
val g = glasses
val want = started && !isInPictureInPictureMode && !runtimeRemovalBlocked && g.usable && glassesSettings.gameOnGlasses
val target = if (want) Glasses.glassesDisplay()?.takeIf { it.isValid } else null
val current = glassesScreen
if (current != null && current.display.displayId == target?.displayId) return
if (current != null) {
glassesScreen = null
glassesOutputActive = false
surfaceW = 0
surfaceH = 0
runCatching { current.dismiss() }
CompositorHost.detach(current.holder.surface)
if (surfaceView.holder.surface?.isValid == true) surfaceCreated(surfaceView.holder)
}
if (target != null) {
val screen = GlassesScreen(this, target, this)
try {
screen.show()
} catch (e: Exception) {
Log.w(TAG, "could not show the game on ${target.name}", e)
return
}
glassesScreen = screen
glassesOutputActive = true
surfaceW = 0
surfaceH = 0
Log.i(TAG, "game output on ${g.model} (display ${target.displayId})")
}
refreshSecondScreenDisplays()
applyCardboard(cardboard)
}
private fun onGlassesSteamVr(now: SteamVr.Status) {
val g = glasses
if (now == SteamVr.Status.RUNNING) {
if (glassesSettings.auto3d && g.can3d && !g.sbs && !glasses3dForVr) {
glasses3dForVr = true
Glasses.set3d(true)
}
} else if (glasses3dForVr && now != SteamVr.Status.UNAVAILABLE) {
glasses3dForVr = false
if (g.sbs) Glasses.set3d(false)
}
}
@@ -2745,6 +2912,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
pendingBackAction = null
if (::pip.isInitialized) pip.destroyed()
closeSecondScreen(reset = false)
glassesScope.cancel()
glassesScreen?.let { screen -> glassesScreen = null; runCatching { screen.dismiss() } }
headTracker?.stop()
if (::hud.isInitialized) hud.stop()
padBridge?.stop()
steamController?.stop()
@@ -101,11 +101,58 @@ class AgentBridgeProvider : ContentProvider() {
AgentEnv.override(context, request.optString("name")).put("ok", true).put("command", "override")
}
"prefs" -> prefs(context, request)
"glasses" -> glasses(context, request)
"access" -> JSONObject().put("ok", true).put("commands", AgentAccess.commandsAllowed(context))
.put("debugBuild", BuildConfig.DEBUG).put("inbox", AgentEnv.inbox(context).absolutePath)
else -> error("UNKNOWN_COMMAND", "Unknown agent command '$method'")
}
private fun glasses(context: Context, request: JSONObject): JSONObject {
val g = com.droiddeck.launcher.glasses.Glasses
when (request.optString("op", "status")) {
"status" -> {}
"allow" -> g.requestControl()
"set" -> {
val key = request.optString("key")
val value = request.optString("value")
fun on() = value == "1" || value.equals("true", true) || value.equals("on", true)
val number = value.toIntOrNull()
val numeric = setOf("refreshHz", "sbsHz", "brightness", "volume", "picture3d", "screenWidthDm", "screenDistanceDm", "sbsSupport")
if (key in numeric && number == null) return error("INVALID_VALUE", "'$key' takes a number")
when (key) {
"refreshHz" -> g.setRefreshHz(number!!)
"sbsHz" -> g.setSbsHz(number!!)
"brightness" -> g.setBrightness(number!!)
"volume" -> g.setVolume(number!!)
"sunblock" -> g.setSunblock(on())
"3d" -> g.set3d(on())
"gameOnGlasses" -> g.setGameOnGlasses(on())
"headTracking" -> g.setHeadTracking(on())
"auto3d" -> g.setAuto3d(on())
"picture3d" -> g.setPicture3d(number!!)
"screenWidthDm" -> g.setScreenWidthDm(number!!)
"screenDistanceDm" -> g.setScreenDistanceDm(number!!)
"sbsSupport" -> g.setSbsSupport(number!!)
else -> return error("INVALID_KEY", "Unknown glasses setting '$key'")
}
}
"simulate" -> if (!g.simulate(request.optInt("seconds", 20).coerceIn(1, 600))) return error("BUSY", "Already simulating")
"stop-simulate" -> g.stopSimulation()
"query" -> { g.query(); Thread.sleep(600) }
"mode" -> { g.sendMode(request.optString("mode").removePrefix("0x").toInt(16)); Thread.sleep(600) }
"imu" -> return g.sampleImu(request.optLong("ms", 2000).coerceIn(200, 20_000)).put("ok", true).put("command", "glasses")
"raw" -> {
AgentAccess.requireCommands(context)
val payload = request.optString("payload").chunked(2).filter { it.length == 2 }.map { it.toInt(16).toByte() }.toByteArray()
if (!g.sendRaw(request.optString("msgId").removePrefix("0x").toInt(16), payload)) return error("NOT_CONNECTED", "The glasses' control link is not open")
Thread.sleep(300)
}
else -> return error("INVALID_OP", "Unknown glasses op")
}
if (request.optString("op") in setOf("set", "allow")) Thread.sleep(400)
return g.status().put("ok", true).put("command", "glasses").put("frames", g.recentFrames())
}
private fun state(context: Context): JSONObject {
val runtimeVersion = LinuxRuntimeInstaller.installedVersion(context)
val dir = SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: SessionPaths.sessionFolders(context).lastOrNull()
@@ -0,0 +1,562 @@
package com.droiddeck.launcher.glasses
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.display.DisplayManager
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.SystemClock
import android.util.Log
import android.view.Display
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONArray
import org.json.JSONObject
import kotlin.math.abs
import kotlin.math.sin
data class GlassesState(
val present: Boolean = false,
val model: String = "VITURE",
val controllable: Boolean = false,
val permission: Glasses.Permission = Glasses.Permission.NONE,
val connected: Boolean = false,
val displayId: Int = Display.INVALID_DISPLAY,
val width: Int = 0,
val height: Int = 0,
val hz: Float = 0f,
val sbs: Boolean = false,
val brightness: Int = -1,
val volume: Int = -1,
val brightnessMax: Int = 8,
val volumeMax: Int = 8,
val sunblock: Boolean = false,
val tracking: Boolean = false,
val diagonalFovDeg: Float = 46f,
val noSignal: Boolean = false,
val sbsSupport: Int = SBS_UNKNOWN,
val sbs90: Boolean = false,
) {
val hasDisplay: Boolean get() = displayId != Display.INVALID_DISPLAY
val usable: Boolean get() = hasDisplay && !noSignal
val canControl: Boolean get() = connected && controllable
val can3d: Boolean get() = canControl && !noSignal && sbsSupport != SBS_FAILS
val verticalFovDeg: Float get() {
val half = Math.toRadians(diagonalFovDeg / 2.0)
val aspect = 16.0 / 9.0
val v = kotlin.math.atan(kotlin.math.tan(half) / kotlin.math.sqrt(1 + aspect * aspect))
return Math.toDegrees(2 * v).toFloat()
}
companion object {
const val SBS_UNKNOWN = 0
const val SBS_WORKS = 1
const val SBS_FAILS = -1
}
}
data class GlassesSettings(
val refreshHz: Int = 120,
val sbsHz: Int = 60,
val sunblock: Boolean? = null,
val gameOnGlasses: Boolean = true,
val headTracking: Boolean = true,
val auto3d: Boolean = true,
val picture3d: Int = PICTURE_AUTO,
val screenWidthDm: Int = 20,
val screenDistanceDm: Int = 30,
) {
companion object {
const val PICTURE_AUTO = 0
const val PICTURE_SCREEN = 1
const val PICTURE_SBS = 2
}
}
@SuppressLint("StaticFieldLeak")
object Glasses {
enum class Permission { NONE, ASKING, DENIED, GRANTED }
fun interface PoseSink {
fun onPose(x: Float, y: Float, z: Float, w: Float, timeNs: Long)
}
private const val TAG = "Glasses"
private const val ACTION_PERMISSION = "com.droiddeck.launcher.glasses.USB_PERMISSION"
private const val PREFS = "glasses"
private const val KEY_SBS_SUPPORT = "sbsSupport"
private const val SBS_CONFIRM_MS = 20_000L
private val _state = MutableStateFlow(GlassesState())
val state: StateFlow<GlassesState> = _state.asStateFlow()
private val _settings = MutableStateFlow(GlassesSettings())
val settings: StateFlow<GlassesSettings> = _settings.asStateFlow()
private lateinit var app: Context
private lateinit var handler: Handler
private lateinit var usb: UsbManager
private lateinit var displays: DisplayManager
@Volatile private var link: VitureLink? = null
private var linkDevice: UsbDevice? = null
private var model: VitureProtocol.Model? = null
private var asked = false
private var lastModeSentAt = 0L
private var sbsAskedAt = 0L
@Volatile private var sink: PoseSink? = null
@Volatile private var probe: ((ByteArray) -> Unit)? = null
@Volatile private var latest: FloatArray? = null
@Volatile private var imuPackets = 0L
private val simulating = java.util.concurrent.atomic.AtomicReference<Thread?>(null)
private val frames = ArrayDeque<String>()
fun init(context: Context) {
if (::handler.isInitialized) return
app = context.applicationContext
usb = app.getSystemService(Context.USB_SERVICE) as UsbManager
displays = app.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
_settings.value = load()
_state.value = _state.value.copy(sbsSupport = app.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getInt(KEY_SBS_SUPPORT, GlassesState.SBS_UNKNOWN))
handler = Handler(HandlerThread("droiddeck-glasses").apply { start() }.looper)
handler.post {
val filter = IntentFilter(ACTION_PERMISSION).apply {
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
}
if (Build.VERSION.SDK_INT >= 33) app.registerReceiver(receiver, filter, null, handler, Context.RECEIVER_NOT_EXPORTED)
else app.registerReceiver(receiver, filter, null, handler)
displays.registerDisplayListener(displayListener, handler)
refresh()
}
}
fun rescan() = post { refresh() }
fun query() = post {
val l = link ?: return@post
listOf(VitureProtocol.MSG_GET_DISPLAY, VitureProtocol.MSG_GET_BRIGHTNESS, VitureProtocol.MSG_GET_VOLUME, VitureProtocol.MSG_GET_IMU).forEachIndexed { i, msg ->
handler.postDelayed({ if (link === l) l.send(msg) }, 60L * i)
}
}
fun sendMode(mode: Int) = post {
if (!_state.value.canControl) return@post
if (link?.send(VitureProtocol.MSG_DISPLAY, byteArrayOf(mode.toByte())) == true) lastModeSentAt = SystemClock.uptimeMillis()
}
fun requestControl() = post {
asked = false
refresh(ask = true)
}
fun setRefreshHz(hz: Int) = post {
update { it.copy(refreshHz = hz.coerceIn(60, 120)) }
if (!_state.value.sbs) applyMode(force = true)
}
fun setSbsHz(hz: Int) = post {
update { it.copy(sbsHz = if (hz >= 90) 90 else 60) }
if (_state.value.sbs && _state.value.can3d) sendMode(true)
}
fun set3d(on: Boolean) = post {
val s = _state.value
if (if (on) s.can3d else s.canControl) sendMode(on)
}
fun setBrightness(level: Int) = post {
val s = _state.value
if (!s.canControl) return@post
val v = level.coerceIn(0, s.brightnessMax)
if (link?.send(VitureProtocol.MSG_BRIGHTNESS, byteArrayOf(v.toByte(), 0)) == true) publish { it.copy(brightness = v) }
}
fun setVolume(level: Int) = post {
val s = _state.value
if (!s.canControl) return@post
val v = level.coerceIn(0, s.volumeMax)
if (link?.send(VitureProtocol.MSG_VOLUME, byteArrayOf(v.toByte(), 0)) == true) publish { it.copy(volume = v) }
}
fun setSunblock(on: Boolean) = post {
update { it.copy(sunblock = on) }
sendFilm(on)
}
fun setSbsSupport(support: Int) = post { rememberSbs(support.coerceIn(GlassesState.SBS_FAILS, GlassesState.SBS_WORKS)) }
fun setGameOnGlasses(on: Boolean) = post { update { it.copy(gameOnGlasses = on) } }
fun setHeadTracking(on: Boolean) = post { update { it.copy(headTracking = on) } }
fun setAuto3d(on: Boolean) = post { update { it.copy(auto3d = on) } }
fun setPicture3d(picture: Int) = post { update { it.copy(picture3d = picture.coerceIn(0, 2)) } }
fun setScreenWidthDm(dm: Int) = post { update { it.copy(screenWidthDm = dm.coerceIn(5, 200)) } }
fun setScreenDistanceDm(dm: Int) = post { update { it.copy(screenDistanceDm = dm.coerceIn(5, 200)) } }
fun glassesDisplay(): Display? = _state.value.displayId.takeIf { it != Display.INVALID_DISPLAY }?.let { displays.getDisplay(it) }
fun setPoseSink(s: PoseSink) {
synchronized(this) { sink = s }
post { updateImu() }
}
fun clearPoseSink(s: PoseSink) {
synchronized(this) { if (sink === s) sink = null }
post { updateImu() }
}
fun simulate(seconds: Int): Boolean {
val t = Thread({
val start = SystemClock.elapsedRealtimeNanos()
val end = start + seconds * 1_000_000_000L
while (SystemClock.elapsedRealtimeNanos() < end && simulating.get() === Thread.currentThread()) {
val now = SystemClock.elapsedRealtimeNanos()
val t = (now - start) / 1e9
val yaw = 40.0 * sin(t * 2 * Math.PI / 6.0)
val pitch = 15.0 * sin(t * 2 * Math.PI / 4.0)
val roll = 5.0 * sin(t * 2 * Math.PI / 5.0)
val q = VitureProtocol.eulerZxy(roll.toFloat(), pitch.toFloat(), yaw.toFloat())
deliver(q, now)
SystemClock.sleep(4)
}
simulating.compareAndSet(Thread.currentThread(), null)
}, "glasses-simulate")
if (!simulating.compareAndSet(null, t)) return false
t.start()
return true
}
fun stopSimulation() {
simulating.set(null)
}
fun status(): JSONObject {
val s = _state.value
val g = _settings.value
val q = latest
return JSONObject()
.put("present", s.present).put("model", s.model).put("controllable", s.controllable)
.put("permission", s.permission.name).put("connected", s.connected)
.put("display", if (s.hasDisplay) JSONObject().put("id", s.displayId).put("width", s.width).put("height", s.height).put("hz", s.hz.toDouble()) else JSONObject.NULL)
.put("sbs", s.sbs).put("brightness", s.brightness).put("volume", s.volume)
.put("brightnessMax", s.brightnessMax).put("volumeMax", s.volumeMax).put("sunblock", s.sunblock)
.put("tracking", s.tracking).put("simulating", simulating.get() != null).put("imuPackets", imuPackets)
.put("pose", q?.let { JSONArray().put(it[0].toDouble()).put(it[1].toDouble()).put(it[2].toDouble()).put(it[3].toDouble()) } ?: JSONObject.NULL)
.put("fovDeg", s.diagonalFovDeg.toDouble()).put("noSignal", s.noSignal).put("sbsSupport", s.sbsSupport)
.put("settings", JSONObject().put("refreshHz", g.refreshHz).put("sbsHz", g.sbsHz).put("sunblock", g.sunblock ?: JSONObject.NULL)
.put("gameOnGlasses", g.gameOnGlasses).put("headTracking", g.headTracking).put("auto3d", g.auto3d)
.put("picture3d", g.picture3d).put("screenWidthDm", g.screenWidthDm).put("screenDistanceDm", g.screenDistanceDm))
}
fun recentFrames(): JSONArray = synchronized(frames) { JSONArray(frames.toList()) }
fun sendRaw(msgId: Int, payload: ByteArray): Boolean {
val l = link ?: return false
return l.send(msgId, payload)
}
private fun post(block: () -> Unit) {
if (!::handler.isInitialized) return
handler.post(block)
}
private fun publish(transform: (GlassesState) -> GlassesState) {
_state.value = transform(_state.value)
}
private fun update(transform: (GlassesSettings) -> GlassesSettings) {
val next = transform(_settings.value)
_settings.value = next
save(next)
}
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_PERMISSION -> {
val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
Log.i(TAG, "USB permission ${if (granted) "granted" else "denied"}")
if (!granted) publish { it.copy(permission = Permission.DENIED) }
refresh()
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
if (intent.device()?.vendorId == VitureProtocol.VENDOR_ID) asked = false
refresh(ask = true)
}
UsbManager.ACTION_USB_DEVICE_DETACHED -> if (intent.device()?.vendorId == VitureProtocol.VENDOR_ID) {
closeLink()
refresh()
}
}
}
}
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = refresh()
override fun onDisplayRemoved(displayId: Int) = refresh()
override fun onDisplayChanged(displayId: Int) {
if (displayId == _state.value.displayId || displayId != Display.DEFAULT_DISPLAY) refresh()
}
}
private fun refresh(ask: Boolean = false) {
val device = usb.deviceList.values.firstOrNull { it.vendorId == VitureProtocol.VENDOR_ID }
val m = device?.let { VitureProtocol.model(it.productId, runCatching { it.productName }.getOrNull()) }
if (device == null || (linkDevice != null && linkDevice?.deviceName != device.deviceName)) closeLink()
model = m
val display = findDisplay(device != null)
val mode = display?.mode
val width = mode?.physicalWidth ?: 0
val height = mode?.physicalHeight ?: 0
val sbsByDisplay = width >= 3000
val noSignal = display != null && width in 1 until 1280
if (sbsAskedAt != 0L) {
when {
SystemClock.uptimeMillis() - sbsAskedAt > SBS_CONFIRM_MS -> sbsAskedAt = 0L
display == null -> Unit
sbsByDisplay -> {
sbsAskedAt = 0L
rememberSbs(GlassesState.SBS_WORKS)
}
noSignal -> {
sbsAskedAt = 0L
Log.w(TAG, "3D came back as ${width}x$height: this phone can't send the side-by-side signal")
rememberSbs(GlassesState.SBS_FAILS)
handler.postDelayed({ sendMode(false) }, 500)
}
}
}
val permitted = device != null && usb.hasPermission(device)
if (device != null && !permitted && (ask || !asked) && _state.value.permission != Permission.ASKING) askPermission(device)
if (device != null && permitted && link == null && m?.controllable == true) openLink(device)
publish {
it.copy(
present = device != null || display != null,
model = m?.name ?: display?.let(::displayName) ?: "VITURE",
controllable = m?.controllable == true,
permission = when {
device == null -> Permission.NONE
permitted -> Permission.GRANTED
it.permission == Permission.ASKING || it.permission == Permission.DENIED -> it.permission
else -> Permission.NONE
},
connected = link != null,
displayId = display?.displayId ?: Display.INVALID_DISPLAY,
width = width, height = height, hz = mode?.refreshRate ?: 0f,
sbs = if (display != null) sbsByDisplay else it.sbs && link != null,
brightnessMax = m?.brightnessMax ?: 8, volumeMax = m?.volumeMax ?: 8,
sunblock = _settings.value.sunblock ?: it.sunblock,
diagonalFovDeg = m?.diagonalFovDeg ?: 46f,
tracking = it.tracking && link != null,
noSignal = noSignal,
sbs90 = m?.sbs90 == true,
)
}
if (link != null && display != null && !noSignal) applyMode(force = false)
}
private fun findDisplay(usbPresent: Boolean): Display? {
val candidates = displays.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
.filter { it.displayId != Display.DEFAULT_DISPLAY && it.flags and Display.FLAG_PRESENTATION != 0 }
return candidates.firstOrNull { displayName(it)?.contains("VITURE", ignoreCase = true) == true }
?: candidates.singleOrNull()?.takeIf { usbPresent }
}
private fun displayName(d: Display): String? {
if (Build.VERSION.SDK_INT >= 31) d.deviceProductInfo?.name?.takeIf { it.isNotBlank() }?.let { return it }
return d.name
}
private fun askPermission(device: UsbDevice) {
asked = true
publish { it.copy(permission = Permission.ASKING) }
val flags = if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0
val pi = PendingIntent.getBroadcast(app, 0, Intent(ACTION_PERMISSION).setPackage(app.packageName), flags)
runCatching { usb.requestPermission(device, pi) }.onFailure { Log.w(TAG, "requestPermission", it) }
}
private fun openLink(device: UsbDevice) {
val l = VitureLink.open(usb, device, linkListener) ?: run {
Log.w(TAG, "could not open ${device.deviceName}")
return
}
link = l
linkDevice = device
Log.i(TAG, "opened ${model?.name} pid=0x%04X imu=%s".format(device.productId, l.hasImu))
publish { it.copy(connected = true) }
listOf(VitureProtocol.MSG_GET_BRIGHTNESS, VitureProtocol.MSG_GET_VOLUME, VitureProtocol.MSG_GET_DISPLAY).forEachIndexed { i, msg ->
handler.postDelayed({ if (link === l) l.send(msg) }, 60L * (i + 1))
}
handler.postDelayed({
if (link !== l) return@postDelayed
_settings.value.sunblock?.let { sendFilm(it) }
updateImu()
applyMode(force = false)
}, 300)
}
private fun closeLink() {
val l = link ?: return
link = null
linkDevice = null
l.close()
publish { it.copy(connected = false, tracking = false) }
Log.i(TAG, "link closed")
}
private fun rememberSbs(support: Int) {
if (_state.value.sbsSupport == support) return
app.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putInt(KEY_SBS_SUPPORT, support).apply()
publish { it.copy(sbsSupport = support) }
}
private fun applyMode(force: Boolean) {
val s = _state.value
if (!s.canControl || !s.usable || s.sbs) return
val want = _settings.value.refreshHz
if (!force && abs(s.hz - want) < 1.5f) return
if (!force && SystemClock.uptimeMillis() - lastModeSentAt < 8_000) return
sendMode(false)
}
private fun sendMode(sbs: Boolean) {
val m = model ?: return
if (sbs && _state.value.sbsSupport == GlassesState.SBS_FAILS) return
val g = _settings.value
val mode = m.mode(if (sbs) g.sbsHz else g.refreshHz, sbs)
if (link?.send(VitureProtocol.MSG_DISPLAY, byteArrayOf(mode.toByte())) == true) {
lastModeSentAt = SystemClock.uptimeMillis()
sbsAskedAt = if (sbs) lastModeSentAt else 0L
}
}
private fun sendFilm(on: Boolean) {
if (!_state.value.canControl) return
if (link?.send(VitureProtocol.MSG_FILM, byteArrayOf(if (on) 1 else 0)) == true) publish { it.copy(sunblock = on) }
}
private fun updateImu() {
val l = link ?: return
val want = (sink != null || probe != null) && l.hasImu
if (want == _state.value.tracking) return
l.send(VitureProtocol.MSG_IMU, byteArrayOf(if (want) 1 else 0))
if (want) l.send(VitureProtocol.MSG_IMU_RATE, byteArrayOf(VitureProtocol.IMU_RATE_240.toByte()))
publish { it.copy(tracking = want) }
}
private val linkListener = object : VitureLink.Listener {
override fun onFrame(frame: VitureProtocol.Frame) {
val line = "%s 0x%04X %s".format(
when (frame.magic) { VitureProtocol.MAGIC_REPORT -> "FD"; VitureProtocol.MAGIC_COMMAND -> "FE"; else -> "FC" },
frame.msgId, frame.payload.joinToString("") { "%02X".format(it) },
)
synchronized(frames) {
frames.addLast(line)
while (frames.size > 64) frames.removeFirst()
}
Log.i(TAG, "frame $line")
post { onReport(frame) }
}
override fun onImu(payload: ByteArray, timeNs: Long) {
imuPackets++
probe?.invoke(payload)
if (simulating.get() != null) return
VitureProtocol.imuQuaternion(payload)?.let { deliver(it, timeNs) }
}
override fun onLost() = post {
closeLink()
refresh()
}
}
private fun deliver(q: FloatArray, timeNs: Long) {
latest = q
sink?.onPose(q[0], q[1], q[2], q[3], timeNs)
}
private fun onReport(frame: VitureProtocol.Frame) {
val p = frame.payload
val event = p.firstOrNull()?.toInt()?.and(0xFF) ?: return
val answer = if (p.size >= 2 && event == 0) p[1].toInt() and 0xFF else null
when (frame.msgId) {
VitureProtocol.EVENT_DISPLAY -> {
Log.i(TAG, "glasses switched to mode 0x%02X".format(event))
publish { it.copy(sbs = VitureProtocol.modeIsSbs(event)) }
}
VitureProtocol.EVENT_BRIGHTNESS -> publish { it.copy(brightness = event) }
VitureProtocol.EVENT_VOLUME -> publish { it.copy(volume = event) }
VitureProtocol.MSG_GET_BRIGHTNESS, VitureProtocol.MSG_BRIGHTNESS -> answer?.let { v -> publish { it.copy(brightness = v) } }
VitureProtocol.MSG_GET_VOLUME, VitureProtocol.MSG_VOLUME -> answer?.let { v -> publish { it.copy(volume = v) } }
VitureProtocol.MSG_DISPLAY -> if (event != 0) {
Log.w(TAG, "glasses refused display mode 0x%02X".format(p.getOrNull(1)?.toInt()?.and(0xFF) ?: 0))
sbsAskedAt = 0L
}
VitureProtocol.MSG_GET_DISPLAY -> answer?.let { v -> if (VitureProtocol.modeIsSbs(v) != _state.value.sbs && !_state.value.hasDisplay) publish { it.copy(sbs = VitureProtocol.modeIsSbs(v)) } }
}
}
fun sampleImu(ms: Long): JSONObject {
val raw = java.util.Collections.synchronizedList(mutableListOf<ByteArray>())
val counter = java.util.concurrent.atomic.AtomicInteger()
val began = SystemClock.elapsedRealtime()
probe = { payload -> val n = counter.incrementAndGet(); if (raw.size < 4 || n % 60 == 0) raw.add(payload.copyOf()) }
post { updateImu() }
SystemClock.sleep(ms)
probe = null
post { updateImu() }
val seconds = (SystemClock.elapsedRealtime() - began) / 1000.0
val count = counter.get()
val samples = JSONArray()
synchronized(raw) {
raw.forEach { b ->
val q = VitureProtocol.imuQuaternion(b)
samples.put(JSONObject()
.put("hex", b.joinToString("") { "%02X".format(it) })
.put("euler", JSONArray().put(VitureProtocol.beFloat(b, 0).toDouble()).put(VitureProtocol.beFloat(b, 4).toDouble()).put(VitureProtocol.beFloat(b, 8).toDouble()))
.put("quat", q?.let { JSONArray().put(it[0].toDouble()).put(it[1].toDouble()).put(it[2].toDouble()).put(it[3].toDouble()) } ?: JSONObject.NULL))
}
}
return JSONObject().put("packets", count).put("rateHz", count / seconds).put("samples", samples)
}
private fun Intent.device(): UsbDevice? =
if (Build.VERSION.SDK_INT >= 33) getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
else @Suppress("DEPRECATION") getParcelableExtra(UsbManager.EXTRA_DEVICE)
private fun load(): GlassesSettings {
val p = app.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
return GlassesSettings(
refreshHz = p.getInt("refreshHz", 120),
sbsHz = p.getInt("sbsHz", 60),
sunblock = if (p.contains("sunblock")) p.getBoolean("sunblock", false) else null,
gameOnGlasses = p.getBoolean("gameOnGlasses", true),
headTracking = p.getBoolean("headTracking", true),
auto3d = p.getBoolean("auto3d", true),
picture3d = p.getInt("picture3d", GlassesSettings.PICTURE_AUTO),
screenWidthDm = p.getInt("screenWidthDm", 20),
screenDistanceDm = p.getInt("screenDistanceDm", 30),
)
}
private fun save(s: GlassesSettings) {
app.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().apply {
putInt("refreshHz", s.refreshHz)
putInt("sbsHz", s.sbsHz)
s.sunblock?.let { putBoolean("sunblock", it) } ?: remove("sunblock")
putBoolean("gameOnGlasses", s.gameOnGlasses)
putBoolean("headTracking", s.headTracking)
putBoolean("auto3d", s.auto3d)
putInt("picture3d", s.picture3d)
putInt("screenWidthDm", s.screenWidthDm)
putInt("screenDistanceDm", s.screenDistanceDm)
}.apply()
}
}
@@ -0,0 +1,33 @@
package com.droiddeck.launcher.glasses
import android.app.Presentation
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.Display
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
class GlassesScreen(context: Context, display: Display, private val callback: SurfaceHolder.Callback) : Presentation(context, display) {
val surfaceView = SurfaceView(getContext())
val holder: SurfaceHolder get() = surfaceView.holder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val root = FrameLayout(context).apply { setBackgroundColor(Color.BLACK) }
root.addView(surfaceView, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
setContentView(root)
window?.apply {
addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
attributes = attributes.apply {
display.supportedModes.maxByOrNull { it.refreshRate * 10_000 + it.physicalWidth }?.let { best ->
if (display.mode.physicalWidth == best.physicalWidth) preferredDisplayModeId = best.modeId
}
}
}
surfaceView.holder.addCallback(callback)
}
}
@@ -0,0 +1,120 @@
package com.droiddeck.launcher.glasses
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.os.SystemClock
import android.util.Log
internal class VitureLink private constructor(
private val connection: UsbDeviceConnection,
private val mcu: UsbInterface,
private val mcuOut: UsbEndpoint,
private val mcuIn: UsbEndpoint?,
private val imu: UsbInterface?,
private val imuIn: UsbEndpoint?,
private val listener: Listener,
) {
interface Listener {
fun onFrame(frame: VitureProtocol.Frame)
fun onImu(payload: ByteArray, timeNs: Long)
fun onLost()
}
@Volatile private var running = true
private val writeLock = Any()
private val readers = mutableListOf<Thread>()
val hasImu: Boolean get() = imuIn != null
private fun startReaders() {
mcuIn?.let { ep -> readers += reader("viture-mcu", ep) { buf, n -> VitureProtocol.parse(buf, n)?.let(listener::onFrame) } }
imuIn?.let { ep ->
readers += reader("viture-imu", ep) { buf, n ->
val frame = VitureProtocol.parse(buf, n) ?: return@reader
if (frame.magic == VitureProtocol.MAGIC_IMU) listener.onImu(frame.payload, SystemClock.elapsedRealtimeNanos())
else listener.onFrame(frame)
}
}
}
private fun reader(name: String, ep: UsbEndpoint, onPacket: (ByteArray, Int) -> Unit): Thread =
Thread({
val buf = ByteArray(VitureProtocol.PACKET_SIZE)
var failures = 0
while (running) {
val started = SystemClock.uptimeMillis()
val n = connection.bulkTransfer(ep, buf, buf.size, READ_TIMEOUT_MS)
if (!running) break
if (n > 0) {
failures = 0
runCatching { onPacket(buf, n) }.onFailure { Log.w(TAG, "$name packet", it) }
} else if (n < 0 && SystemClock.uptimeMillis() - started < READ_TIMEOUT_MS / 2) {
failures++
SystemClock.sleep(2)
if (failures >= LOST_AFTER) {
Log.w(TAG, "$name: reads keep failing, link lost")
if (running) listener.onLost()
break
}
}
}
}, name).apply { isDaemon = true; priority = Thread.MAX_PRIORITY; start() }
fun send(msgId: Int, payload: ByteArray = ByteArray(0)): Boolean {
val pkt = VitureProtocol.packet(msgId, payload)
val n = synchronized(writeLock) {
if (!running) return false
connection.bulkTransfer(mcuOut, pkt, pkt.size, WRITE_TIMEOUT_MS)
}
Log.i(TAG, "send 0x%04X [%s] -> %d".format(msgId, payload.joinToString("") { "%02X".format(it) }, n))
return n == pkt.size
}
fun close() {
synchronized(writeLock) { running = false }
val self = Thread.currentThread()
readers.forEach { if (it !== self) runCatching { it.join(READ_TIMEOUT_MS * 4L) } }
runCatching { connection.releaseInterface(mcu) }
imu?.let { runCatching { connection.releaseInterface(it) } }
runCatching { connection.close() }
}
companion object {
private const val TAG = "VitureLink"
private const val READ_TIMEOUT_MS = 100
private const val WRITE_TIMEOUT_MS = 250
private const val LOST_AFTER = 50
fun open(usb: UsbManager, device: UsbDevice, listener: Listener): VitureLink? {
val hid = (0 until device.interfaceCount).map(device::getInterface)
.filter { it.interfaceClass == UsbConstants.USB_CLASS_HID }
.sortedBy { it.id }
for (intf in hid) Log.i(TAG, "iface ${intf.id} eps " + (0 until intf.endpointCount).joinToString {
val ep = intf.getEndpoint(it)
"0x%02X".format(ep.address)
})
val mcu = hid.lastOrNull { it.outEndpoint() != null } ?: return null
val imu = hid.firstOrNull { it.id != mcu.id && it.inEndpoint() != null }
val conn = usb.openDevice(device) ?: return null
if (!conn.claimInterface(mcu, true)) {
conn.close()
return null
}
val imuClaimed = imu != null && conn.claimInterface(imu, true)
val link = VitureLink(conn, mcu, mcu.outEndpoint()!!, mcu.inEndpoint(),
imu.takeIf { imuClaimed }, imu.takeIf { imuClaimed }?.inEndpoint(), listener)
link.startReaders()
return link
}
private fun UsbInterface.outEndpoint(): UsbEndpoint? = (0 until endpointCount).map(::getEndpoint)
.firstOrNull { it.direction == UsbConstants.USB_DIR_OUT && it.type == UsbConstants.USB_ENDPOINT_XFER_INT }
private fun UsbInterface.inEndpoint(): UsbEndpoint? = (0 until endpointCount).map(::getEndpoint)
.firstOrNull { it.direction == UsbConstants.USB_DIR_IN && it.type == UsbConstants.USB_ENDPOINT_XFER_INT }
}
}
@@ -0,0 +1,169 @@
package com.droiddeck.launcher.glasses
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
object VitureProtocol {
const val VENDOR_ID = 0x35CA
const val PACKET_SIZE = 64
private const val HEADER = 0x12
const val MAGIC_COMMAND = 0xFE
const val MAGIC_REPORT = 0xFD
const val MAGIC_IMU = 0xFC
const val MSG_GET_BRIGHTNESS = 0x0005
const val MSG_BRIGHTNESS = 0x0006
const val MSG_GET_DISPLAY = 0x0007
const val MSG_DISPLAY = 0x0008
const val MSG_FILM = 0x000E
const val MSG_IMU = 0x0015
const val MSG_GET_IMU = 0x0017
const val MSG_IMU_RATE = 0x0018
const val MSG_GET_VOLUME = 0x0032
const val MSG_VOLUME = 0x0033
const val EVENT_BRIGHTNESS = 0x0301
const val EVENT_DISPLAY = 0x0302
const val EVENT_VOLUME = 0x0304
const val IMU_RATE_240 = 3
enum class Family { ONE, PRO, LUMA, ULTRA, BEAST }
class Model(val name: String, val family: Family) {
val controllable: Boolean get() = family != Family.BEAST
val tall: Boolean get() = family == Family.LUMA || family == Family.ULTRA
val sbs90: Boolean get() = tall
val brightnessMax: Int get() = 8
val volumeMax: Int get() = 8
val diagonalFovDeg: Float get() = when (family) {
Family.ONE -> 43f
Family.PRO -> 46f
Family.LUMA, Family.ULTRA -> 50f
Family.BEAST -> 58f
}
fun mode(hz: Int, sbs: Boolean): Int = when {
tall && sbs -> if (hz >= 90) 0x45 else 0x42
tall -> when { hz >= 120 -> 0x44; hz >= 90 -> 0x43; else -> 0x41 }
sbs -> if (hz >= 90 && sbs90) 0x35 else 0x32
else -> when { hz >= 120 -> 0x34; hz >= 90 -> 0x33; else -> 0x31 }
}
}
fun model(productId: Int, productName: String?): Model? {
val family = when (productId) {
0x1011, 0x1013, 0x1015, 0x1017, 0x101b -> Family.ONE
0x1019, 0x101d -> Family.PRO
0x1121, 0x1131, 0x1141, 0x1151 -> Family.LUMA
0x1101, 0x1104 -> Family.ULTRA
0x1201, 0x1211 -> Family.BEAST
else -> return null
}
val fallback = when (productId) {
0x1015, 0x101b -> "VITURE One Lite"
0x1011, 0x1013, 0x1017 -> "VITURE One"
0x1019 -> "VITURE Pro"
0x101d -> "VITURE Pro XR"
0x1131 -> "VITURE Luma"
0x1121, 0x1141, 0x1151 -> "VITURE Luma Pro"
0x1101, 0x1104 -> "VITURE Luma Ultra"
else -> "VITURE Beast"
}
val name = productName?.trim()?.removeSuffix(" Glasses")?.takeIf { it.startsWith("VITURE", ignoreCase = true) } ?: fallback
return Model(name, family)
}
fun modeIsSbs(mode: Int): Boolean = mode == 0x32 || mode == 0x35 || mode == 0x42 || mode == 0x45
fun modeHz(mode: Int): Int = when (mode) {
0x34, 0x44 -> 120
0x33, 0x35, 0x43, 0x45 -> 90
else -> 60
}
private val CRC_TABLE = IntArray(256) { i ->
var c = i shl 8
repeat(8) { c = if (c and 0x8000 != 0) (c shl 1) xor 0x1021 else c shl 1 }
c and 0xFFFF
}
fun crc16(data: ByteArray, offset: Int, length: Int): Int {
var crc = 0
for (i in offset until offset + length) {
crc = (CRC_TABLE[((data[i].toInt() and 0xFF) xor (crc ushr 8)) and 0xFF] xor (crc shl 8)) and 0xFFFF
}
return crc
}
fun packet(msgId: Int, payload: ByteArray = ByteArray(0)): ByteArray {
require(payload.size <= PACKET_SIZE - HEADER)
val buf = ByteArray(PACKET_SIZE)
buf[0] = 0xFF.toByte()
buf[1] = MAGIC_COMMAND.toByte()
val len = payload.size + 0x0C
buf[4] = len.toByte()
buf[5] = (len shr 8).toByte()
buf[14] = msgId.toByte()
buf[15] = (msgId shr 8).toByte()
payload.copyInto(buf, HEADER)
val crc = crc16(buf, 4, len + 2)
buf[2] = crc.toByte()
buf[3] = (crc shr 8).toByte()
return buf
}
class Frame(val magic: Int, val msgId: Int, val payload: ByteArray, val crcOk: Boolean)
fun parse(buf: ByteArray, size: Int): Frame? {
if (size < HEADER || (buf[0].toInt() and 0xFF) != 0xFF) return null
val magic = buf[1].toInt() and 0xFF
if (magic != MAGIC_COMMAND && magic != MAGIC_REPORT && magic != MAGIC_IMU) return null
val len = u16(buf, 4)
if (len < 0x0C) return null
val dataLen = (len - 0x0C).coerceAtMost(size - HEADER)
val crcOk = len + 6 <= size && crc16(buf, 4, len + 2) == u16(buf, 2)
return Frame(magic, u16(buf, 14), buf.copyOfRange(HEADER, HEADER + dataLen), crcOk)
}
private fun u16(buf: ByteArray, at: Int) = (buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)
fun beFloat(b: ByteArray, at: Int): Float = java.lang.Float.intBitsToFloat(
((b[at].toInt() and 0xFF) shl 24) or ((b[at + 1].toInt() and 0xFF) shl 16) or
((b[at + 2].toInt() and 0xFF) shl 8) or (b[at + 3].toInt() and 0xFF),
)
fun imuQuaternion(payload: ByteArray): FloatArray? {
if (payload.size >= 36) {
val w = beFloat(payload, 20)
val x = beFloat(payload, 24)
val y = beFloat(payload, 28)
val z = beFloat(payload, 32)
val n = sqrt(w * w + x * x + y * y + z * z)
if (n.isFinite() && n > 0.9f && n < 1.1f) return floatArrayOf(x / n, y / n, z / n, w / n)
}
if (payload.size < 12) return null
val roll = beFloat(payload, 0)
val pitch = beFloat(payload, 4)
val yaw = beFloat(payload, 8)
if (!roll.isFinite() || !pitch.isFinite() || !yaw.isFinite()) return null
return eulerZxy(roll, pitch, yaw)
}
fun eulerZxy(rollDeg: Float, pitchDeg: Float, yawDeg: Float): FloatArray {
val r = Math.toRadians(rollDeg.toDouble()) / 2
val p = Math.toRadians(pitchDeg.toDouble()) / 2
val y = Math.toRadians(yawDeg.toDouble()) / 2
val cx = cos(r); val sx = sin(r)
val cy = cos(p); val sy = sin(p)
val cz = cos(y); val sz = sin(y)
return floatArrayOf(
(sx * cy * cz - cx * sy * sz).toFloat(),
(cx * sy * cz + sx * cy * sz).toFloat(),
(cx * cy * sz + sx * sy * cz).toFloat(),
(cx * cy * cz - sx * sy * sz).toFloat(),
)
}
}
@@ -26,6 +26,7 @@ data class Cardboard(
val screenWidthDm: Int = 40,
val screenDistanceDm: Int = 30,
val headTracking: Boolean = true,
val mono: Boolean = false,
) {
val lens: Viewer get() = Viewer.byId(viewer)
@@ -35,10 +36,11 @@ data class Cardboard(
* from its viewport's centre, so the two pictures line up with the viewer's lenses; the height,
* seen through the lens from its focal distance, is the field of view the distortion works in.
*/
fun push(panelWidthMm: Float, panelHeightMm: Float) {
fun push(panelWidthMm: Float, panelHeightMm: Float, fovOverrideDeg: Float = 0f, displayHz: Float = 0f) {
val l = lens
WaylandCompositor.nativeSetCardboard(enabled, source, ipdMm / 1000f, screenDistanceDm / 10f,
screenWidthDm / 10f, l.k1, l.k2, fovDeg(l, panelHeightMm), lensInset(panelWidthMm, l.lensSeparationMm), l.chroma)
screenWidthDm / 10f, l.k1, l.k2, if (fovOverrideDeg > 0f) fovOverrideDeg else fovDeg(l, panelHeightMm),
if (mono) 0f else lensInset(panelWidthMm, l.lensSeparationMm), l.chroma, mono, displayHz)
}
fun encode(): JSONObject = JSONObject()
@@ -57,7 +59,8 @@ data class Cardboard(
/** The original 2014 Cardboard: narrower, gentler lenses. */
CARDBOARD_V1("cardboard1", 0.441f, 0.156f, 42f, 60f, 0.004f, 80f),
/** No lens: side-by-side 3D glasses, a 3D TV, or just looking at the stereo pair. */
NONE("none", 0f, 0f, 0f, 0f, 0f, 60f);
NONE("none", 0f, 0f, 0f, 0f, 0f, 60f),
VITURE("viture", 0f, 0f, 0f, 0f, 0f, 23.3f);
companion object {
fun byId(id: String): Viewer = entries.firstOrNull { it.id == id } ?: CARDBOARD_V2
@@ -191,6 +194,10 @@ object HeadPose {
a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c]
}
private val GLASSES_AXES = floatArrayOf(0f, 0f, -1f, -1f, 0f, 0f, 0f, 1f, 0f)
fun fromGlasses(x: Float, y: Float, z: Float, w: Float): FloatArray = multiply(fromQuaternion(x, y, z, w), GLASSES_AXES)
fun transpose(m: FloatArray): FloatArray = floatArrayOf(m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8])
/** m * v for a 3-vector. */
@@ -19,20 +19,43 @@ import com.droiddeck.launcher.wayland.WaylandCompositor
*
* [rotation] is the Display rotation (Surface.ROTATION_*) the session is drawn in.
*/
class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEventListener {
class HeadTracker(context: Context, private val glasses: Boolean = false, private val rotation: () -> Int) : SensorEventListener {
private val sensors = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val sensor: Sensor? = sensors.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR)
?: sensors.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
private var thread: HandlerThread? = null
private var started = false
private val glassesLock = Any()
@Volatile private var reference: HeadPose.Reference? = null
@Volatile private var recenterPending = true
@Volatile private var latest: FloatArray? = null
private val glassesSink = com.droiddeck.launcher.glasses.Glasses.PoseSink { x, y, z, w, _ ->
val m = HeadPose.fromGlasses(x, y, z, w)
synchronized(glassesLock) {
if (!started) return@PoseSink
latest = m
apply(m)
}
}
val usesGlasses: Boolean get() = glasses
/** Whether this device has a rotation sensor at all (no sensor = a fixed, head-locked screen). */
val available: Boolean get() = sensor != null
val sensorName: String get() = sensor?.let { "${it.name} (${if (it.type == Sensor.TYPE_GAME_ROTATION_VECTOR) "game rotation vector" else "rotation vector"})" } ?: "none"
fun start() {
if (glasses) {
synchronized(glassesLock) {
if (started) return
started = true
recenterPending = true
}
com.droiddeck.launcher.glasses.Glasses.setPoseSink(glassesSink)
WaylandCompositor.nativeLog("cardboard", "head tracking on: VITURE glasses IMU")
return
}
val s = sensor ?: run {
WaylandCompositor.nativeLog("cardboard", "no rotation sensor: the virtual screen stays head-locked")
WaylandCompositor.nativeSetCardboardPose(HeadPose.IDENTITY, false)
@@ -47,6 +70,15 @@ class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEve
}
fun stop() {
if (glasses) {
synchronized(glassesLock) {
if (!started) return
started = false
WaylandCompositor.nativeSetCardboardPose(HeadPose.IDENTITY, false)
}
com.droiddeck.launcher.glasses.Glasses.clearPoseSink(glassesSink)
return
}
val t = thread ?: return
sensors.unregisterListener(this)
t.quitSafely()
@@ -57,7 +89,8 @@ class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEve
/** Make where the viewer looks now straight ahead (see HeadPose.recenter). */
fun recenter() {
recenterPending = true
latest?.let { apply(it) }
if (glasses) synchronized(glassesLock) { if (started) latest?.let { apply(it) } }
else latest?.let { apply(it) }
}
override fun onSensorChanged(event: SensorEvent) {
@@ -92,6 +92,7 @@ class OnScreenControls(
private val groups = controls.map { it.group }.distinct()
private var buttonsOnly = false
val isButtonsOnly: Boolean get() = buttonsOnly
private var quickHidden = false
private var quickPressedBy = -1
private var keyboardPressedBy = -1
@@ -0,0 +1,353 @@
package com.droiddeck.launcher.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.lerp
import androidx.compose.ui.zIndex
import com.droiddeck.launcher.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
private const val DROP_PIECES = 10
internal class DropSheetState(private val scope: CoroutineScope) {
var open by mutableStateOf(false)
var shown by mutableStateOf(false)
var settledByDrag = false
var engaged = false
private set
val lift = Animatable(0f)
val ride = Animatable(0f)
val sink = Animatable(0f)
val hot = Animatable(1f)
val pieces = List(DROP_PIECES) { Animatable(0f) }
var travelPx = 1f
private var from = 0f
private var progress = 0f
private var dragJob: Job? = null
fun cancelDrag() {
dragJob?.cancel()
dragJob = null
}
fun reset() {
cancelDrag()
engaged = false
settledByDrag = false
open = false
shown = false
scope.launch {
listOf(lift, ride, sink).forEach { it.snapTo(0f) }
pieces.forEach { it.snapTo(0f) }
hot.snapTo(1f)
}
}
fun beginDrag() {
cancelDrag()
settledByDrag = false
engaged = true
from = if (open) 1f else 0f
progress = from
if (!open) shown = true
}
fun drag(dy: Float) {
progress = (progress + dy / travelPx).coerceIn(0f, 1.04f)
val p = progress
scope.launch {
lift.snapTo(p); ride.snapTo(p); sink.snapTo(p.coerceIn(0f, 1f)); hot.snapTo(0f)
pieces.forEach { it.snapTo(p.coerceIn(0f, 1f)) }
}
}
fun settle(v: Float, flick: Float) {
engaged = false
val opens = when {
v > flick -> true
v < -flick -> false
from > 0.5f -> progress > 0.7f
else -> progress > 0.3f
}
settledByDrag = true
cancelDrag()
dragJob = scope.launch {
val to = if (opens) 1f else 0f
coroutineScope {
launch { lift.animateTo(to, Motion.sp(if (opens) 0.7f else 1f, 380f)) }
launch { ride.animateTo(to, Motion.sp(if (opens) 0.7f else 0.55f, if (opens) 380f else 300f)) }
launch { sink.animateTo(to, Motion.tw(220)) }
pieces.forEachIndexed { i, piece -> launch { if (opens) delay(Motion.ms(i * 35).toLong()); piece.animateTo(to, Motion.tw(if (opens) 200 else 90)) } }
}
if (!opens) { shown = false; hot.snapTo(1f) }
}
if (open != opens) open = opens else settledByDrag = false
}
}
@Composable
internal fun rememberDropSheetState(): DropSheetState {
val scope = rememberCoroutineScope()
return remember { DropSheetState(scope) }
}
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
@Composable
internal fun BoxWithConstraintsScope.DropSheet(
state: DropSheetState,
title: String,
icon: ImageVector,
label: String,
covered: () -> Float = { 0f },
rows: @Composable () -> Unit,
) {
val density = LocalDensity.current
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
val inputMode = LocalInputModeManager.current
val firstRow = remember { FocusRequester() }
val buttonFocus = remember { FocusRequester() }
var rest by remember { mutableStateOf<Rect?>(null) }
fun focusButton() { if (inputMode.inputMode == InputMode.Keyboard) runCatching { buttonFocus.requestFocus() } }
fun close() {
if (!state.open) return
state.open = false
focusButton()
}
val lifecycle = androidx.compose.ui.platform.LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) {
val obs = androidx.lifecycle.LifecycleEventObserver { _, e ->
if (e == androidx.lifecycle.Lifecycle.Event.ON_STOP && (state.open || state.shown || state.engaged)) state.reset()
}
lifecycle.addObserver(obs)
onDispose { lifecycle.removeObserver(obs) }
}
LaunchedEffect(state.open) {
if (state.settledByDrag) {
state.settledByDrag = false
if (state.open) {
if (inputMode.inputMode == InputMode.Keyboard) { withFrameNanos { }; runCatching { firstRow.requestFocus() } }
} else focusButton()
return@LaunchedEffect
}
if (state.engaged) return@LaunchedEffect
state.cancelDrag()
if (state.open) {
state.shown = true
coroutineScope {
launch { state.sink.animateTo(1f, Motion.tw(260)) }
launch {
delay(Motion.ms(120).toLong())
launch { state.lift.animateTo(1f, Motion.sp(0.7f, 380f)) }
launch { state.ride.animateTo(1f, Motion.sp(0.7f, 380f)) }
launch { state.hot.snapTo(1f); delay(Motion.ms(100).toLong()); state.hot.animateTo(0f, Motion.tw(200)) }
}
state.pieces.forEachIndexed { i, p -> launch { delay(Motion.ms(120 + 260 + i * 55).toLong()); p.animateTo(1f, Motion.tw(220)) } }
if (inputMode.inputMode == InputMode.Keyboard) launch {
delay(Motion.ms(300).toLong())
withFrameNanos { }
runCatching { firstRow.requestFocus() }
}
}
} else if (state.shown) {
coroutineScope {
state.pieces.forEach { launch { it.animateTo(0f, Motion.tw(90)) } }
launch { delay(Motion.ms(60).toLong()); state.lift.animateTo(0f, Motion.sp(1f, 380f)) }
launch { delay(Motion.ms(100).toLong()); state.ride.animateTo(0f, Motion.sp(0.55f, 300f)) }
launch { state.sink.animateTo(0f, Motion.tw(300)) }
}
state.shown = false
state.hot.snapTo(1f)
} else {
listOf(state.sink, state.lift, state.ride).forEach { it.snapTo(0f) }
state.pieces.forEach { it.snapTo(0f) }
state.hot.snapTo(1f)
}
}
BackHandler(enabled = state.open) { close() }
val w = constraints.maxWidth.toFloat()
val h = constraints.maxHeight.toFloat()
val inset = with(density) { 12.dp.toPx() }
val edge = with(density) { 3.dp.toPx() }
val buttonH = rest?.height ?: with(density) { 48.dp.toPx() }
val openBottom = h - sheetTop(h, buttonH, with(density) { 16.dp.toPx() }, edge)
val lower = rest?.let { (openBottom - edge - buttonH / 2f) - it.top } ?: 0f
state.travelPx = openBottom.coerceAtLeast(1f)
if (state.shown || state.open) Box(
Modifier.fillMaxSize()
.graphicsLayer { alpha = state.sink.value }.background(Color.Black.copy(alpha = 0.42f))
.focusProperties { canFocus = false }
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, enabled = state.open) { close() },
)
if (state.shown) {
val bottom = lerp(0f, openBottom, state.lift.value)
val corner = with(density) { 18.dp.toPx() }
val rim = with(density) { 1.4.dp.toPx() }
val path = remember { Path() }
val rimPath = remember { Path() }
Canvas(Modifier.fillMaxSize()) {
val l = inset
val r = w - inset
val t = -corner
rimPath.reset()
rimPath.moveTo(l, t)
rimPath.lineTo(l, bottom - corner)
rimPath.arcTo(Rect(l, bottom - 2 * corner, l + 2 * corner, bottom), 180f, -90f, false)
rimPath.lineTo(r - corner, bottom)
rimPath.arcTo(Rect(r - 2 * corner, bottom - 2 * corner, r, bottom), 90f, -90f, false)
rimPath.lineTo(r, t)
path.reset()
path.addPath(rimPath)
path.close()
drawPath(path, lerp(colors.surface, pal.signal, 0.22f * state.hot.value))
drawPath(rimPath, pal.signal.copy(alpha = lerp(0.55f, 0.9f, state.hot.value)), style = Stroke(rim))
}
val sheetW = w - 2 * inset
val rowsTop = with(density) { 62.dp.toPx() }
val rowsBottom = openBottom - edge - buttonH / 2f - with(density) { 8.dp.toPx() }
Box(
Modifier
.offset { IntOffset(inset.roundToInt(), (bottom - openBottom).roundToInt()) }
.size(with(density) { sheetW.toDp() }, with(density) { openBottom.toDp() })
.pointerInput(Unit) { detectTapGestures { } }
.controllerBack { close() }
.focusProperties { exit = { if (state.open) FocusRequester.Cancel else FocusRequester.Default } }
.focusGroup(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.offset { IntOffset(0, 18.dp.roundToPx()) }
.fillMaxWidth()
.padding(start = 20.dp, end = 64.dp)
.graphicsLayer { val v = state.pieces[0].value; alpha = v; translationY = (1f - v) * -6.dp.toPx() },
) {
Icon(icon, null, tint = pal.signal, modifier = Modifier.size(22.dp))
Spacer(Modifier.width(10.dp))
Text(title, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Column(
Modifier
.offset { IntOffset(0, rowsTop.roundToInt()) }
.fillMaxWidth()
.height(with(density) { (rowsBottom - rowsTop).coerceAtLeast(0f).toDp() })
.focusRequester(firstRow)
.focusGroup()
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),
) {
CompositionLocalProvider(LocalSheetBeat provides remember { SheetBeat(state.pieces) }) { rows() }
}
val closeSource = remember { MutableInteractionSource() }
val closeHot = rememberHot(closeSource)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.offset { IntOffset((sheetW - 52.dp.toPx()).roundToInt(), 8.dp.roundToPx()) }
.zIndex(1f).size(44.dp).testTag("drop-close")
.clip(CircleShape).glideBorder(closeHot, CircleShape, pal.signal)
.hoverable(closeSource)
.clickable(interactionSource = closeSource, indication = LocalIndication.current, enabled = state.open, role = Role.Button) { close() }
.controllerConfirm(enabled = state.open) { close() },
) { Icon(Icons.Filled.Close, stringResource(R.string.common_close), tint = colors.onSurfaceVariant, modifier = Modifier.size(20.dp)) }
}
}
val src = remember { MutableInteractionSource() }
val hot = rememberHot(src)
val toggle = { if (state.open) close() else state.open = true }
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 16.dp, end = 20.dp)
.onGloballyPositioned { rest = it.boundsInParent() }
.graphicsLayer { translationY = lower * state.ride.value; alpha = 1f - covered() }
.zIndex(2f)
.size(48.dp)
.testTag("glasses-open")
.focusRequester(buttonFocus)
.clip(CircleShape)
.background(lerp(colors.surface.copy(alpha = 0.82f), pal.signal, 0.22f * state.ride.value))
.glideBorder(hot, CircleShape, pal.signal, pal.line)
.hoverable(src)
.focusProperties { canFocus = covered() < 0.5f }
.clickable(interactionSource = src, indication = LocalIndication.current, enabled = covered() < 0.5f, role = Role.Button, onClick = toggle)
.controllerConfirm(enabled = covered() < 0.5f, onClick = toggle),
) {
Icon(icon, label, tint = if (hot || state.open) colors.onBackground else colors.onSurfaceVariant, modifier = Modifier.size(26.dp))
}
}
@@ -0,0 +1,175 @@
package com.droiddeck.launcher.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.collectAsState
import com.droiddeck.launcher.R
import com.droiddeck.launcher.glasses.Glasses
import com.droiddeck.launcher.glasses.GlassesSettings
import com.droiddeck.launcher.glasses.GlassesState
import java.util.Locale
internal val GlassesIcon: ImageVector by lazy {
ImageVector.Builder("Glasses", 24.dp, 24.dp, 24f, 24f).apply {
path(fill = null, stroke = SolidColor(Color.Black), strokeLineWidth = 1.8f, strokeLineCap = StrokeCap.Round, strokeLineJoin = StrokeJoin.Round) {
moveTo(2.5f, 10f); lineTo(21.5f, 10f)
moveTo(3.2f, 10f); lineTo(3.8f, 15.2f); quadTo(4f, 17f, 6f, 17f); lineTo(8.6f, 17f); quadTo(10.2f, 17f, 10.6f, 15.4f); lineTo(11.2f, 12.6f)
quadTo(12f, 11.6f, 12.8f, 12.6f); lineTo(13.4f, 15.4f); quadTo(13.8f, 17f, 15.4f, 17f); lineTo(18f, 17f); quadTo(20f, 17f, 20.2f, 15.2f); lineTo(20.8f, 10f)
moveTo(2.5f, 10f); lineTo(1.8f, 7.6f)
moveTo(21.5f, 10f); lineTo(22.2f, 7.6f)
}
}.build()
}
@Composable
internal fun rememberGlasses(): Pair<GlassesState, GlassesSettings> {
val g by Glasses.state.collectAsState()
val s by Glasses.settings.collectAsState()
return g to s
}
@Composable
internal fun GlassesGroups(
host: MenuHost,
g: GlassesState,
s: GlassesSettings,
track: (String) -> Modifier = { Modifier },
onRecenter: (() -> Unit)? = null,
inSession: Boolean = true,
steamVr: @Composable () -> Unit = {},
) {
SettingsGroup(g.model) {
val status = if (g.hasDisplay) stringResource(R.string.glasses_status, g.width, g.height, kotlin.math.round(g.hz).toInt(),
stringResource(if (g.sbs) R.string.glasses_mode_3d else R.string.glasses_mode_2d)) else null
when {
g.noSignal -> SettingsRow(stringResource(R.string.glasses_output), stringResource(R.string.glasses_no_signal)) {}
!g.controllable && g.present && g.permission == Glasses.Permission.NONE && !g.connected ->
SettingsRow(stringResource(R.string.glasses_control), status ?: stringResource(R.string.glasses_no_control)) {}
g.permission == Glasses.Permission.ASKING ->
SettingsRow(stringResource(R.string.glasses_control), stringResource(R.string.glasses_asking)) {}
!g.connected && g.controllable -> ActionRow(stringResource(R.string.glasses_control),
stringResource(if (g.permission == Glasses.Permission.DENIED) R.string.glasses_denied else R.string.glasses_allow_hint),
stringResource(R.string.glasses_allow), { Glasses.requestControl() })
status != null -> SettingsRow(stringResource(R.string.glasses_output), status) {}
}
ToggleRow(host, "glasses-game", stringResource(R.string.glasses_game_on), stringResource(R.string.glasses_game_on_hint),
s.gameOnGlasses, chipModifier = track("glasses-game")) { Glasses.setGameOnGlasses(it) }
}
if (g.canControl) SettingsGroup(stringResource(R.string.glasses_display)) {
ChoiceRow(host, "glasses-hz", stringResource(R.string.glasses_refresh), null,
listOf(60, 90, 120).map { it to "$it Hz" }, s.refreshHz, chipModifier = track("glasses-hz")) { Glasses.setRefreshHz(it) }
if (g.sbsSupport == GlassesState.SBS_FAILS && !g.sbs) {
SettingsRow(stringResource(R.string.glasses_3d), stringResource(R.string.glasses_3d_unsupported)) {}
} else {
if (inSession || g.sbs) ToggleRow(host, "glasses-3d", stringResource(R.string.glasses_3d), stringResource(R.string.glasses_3d_hint),
g.sbs, chipModifier = track("glasses-3d")) { Glasses.set3d(it) }
if (g.sbs90) ChoiceRow(host, "glasses-3d-hz", stringResource(R.string.glasses_3d_refresh), null,
listOf(60, 90).map { it to "$it Hz" }, s.sbsHz, chipModifier = track("glasses-3d-hz")) { Glasses.setSbsHz(it) }
ToggleRow(host, "glasses-auto3d", stringResource(R.string.glasses_auto3d), stringResource(R.string.glasses_auto3d_hint),
s.auto3d, chipModifier = track("glasses-auto3d")) { Glasses.setAuto3d(it) }
}
ChoiceRow(host, "glasses-picture", stringResource(R.string.glasses_picture), stringResource(R.string.glasses_picture_hint),
listOf(
GlassesSettings.PICTURE_AUTO to stringResource(R.string.glasses_picture_auto),
GlassesSettings.PICTURE_SCREEN to stringResource(R.string.glasses_picture_screen),
GlassesSettings.PICTURE_SBS to stringResource(R.string.glasses_picture_sbs),
), s.picture3d, chipModifier = track("glasses-picture"), hintLines = 2) { Glasses.setPicture3d(it) }
if (s.picture3d != GlassesSettings.PICTURE_SBS) {
SliderRow(stringResource(R.string.glasses_screen_size), null, s.screenWidthDm, 5..60, step = 1,
format = { String.format(Locale.US, "%.1f m", it / 10f) }, modifier = track("glasses-width")) { Glasses.setScreenWidthDm(it) }
SliderRow(stringResource(R.string.glasses_screen_distance), null, s.screenDistanceDm, 5..100, step = 1,
format = { String.format(Locale.US, "%.1f m", it / 10f) }, modifier = track("glasses-distance")) { Glasses.setScreenDistanceDm(it) }
}
steamVr()
}
if (g.canControl) SettingsGroup(stringResource(R.string.glasses_hardware)) {
SliderRow(stringResource(R.string.glasses_brightness), null, g.brightness.coerceAtLeast(0), 0..g.brightnessMax, step = 1,
modifier = track("glasses-brightness")) { Glasses.setBrightness(it) }
SliderRow(stringResource(R.string.glasses_volume), null, g.volume.coerceAtLeast(0), 0..g.volumeMax, step = 1,
modifier = track("glasses-volume")) { Glasses.setVolume(it) }
ToggleRow(host, "glasses-sunblock", stringResource(R.string.glasses_sunblock), stringResource(R.string.glasses_sunblock_hint),
g.sunblock, chipModifier = track("glasses-sunblock")) { Glasses.setSunblock(it) }
}
if (g.canControl) SettingsGroup(stringResource(R.string.glasses_tracking)) {
ToggleRow(host, "glasses-tracking", stringResource(R.string.glasses_head_tracking), stringResource(R.string.glasses_head_tracking_hint),
s.headTracking, chipModifier = track("glasses-tracking")) { Glasses.setHeadTracking(it) }
if (onRecenter != null && s.headTracking) ActionRow(stringResource(R.string.glasses_recenter), null,
stringResource(R.string.cardboard_recenter_button), onRecenter)
}
}
@Composable
internal fun GlassesCornerButton(slot: Int, tint: Int, opacity: Int, onClick: () -> Unit) {
val density = androidx.compose.ui.platform.LocalDensity.current
androidx.compose.foundation.layout.BoxWithConstraints(
Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.displayCutout),
) {
val x = if (slot < 0) maxWidth / 2 + 56.dp else 34.dp + 56.dp * slot
val bottom = if (slot < 0) 44.dp else 34.dp
val source = remember { MutableInteractionSource() }
val pressed by source.collectIsPressedAsState()
val base = Color(tint)
val alpha = opacity.coerceIn(0, 100) / 100f
val fill = Color(base.red * 0.12f, base.green * 0.12f, base.blue * 0.12f, 190 / 255f)
fun light(a: Int, f: Float) = Color(base.red + (1 - base.red) * f, base.green + (1 - base.green) * f, base.blue + (1 - base.blue) * f, a * alpha / 255f)
val line = if (pressed) light(230, 0.7f) else light(150, 0.25f)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.offset { with(density) { IntOffset((x - 18.dp).roundToPx(), (maxHeight - bottom - 18.dp).roundToPx()) } }
.size(36.dp)
.clip(CircleShape)
.background(fill)
.border(1.5.dp, line, CircleShape)
.clickable(interactionSource = source, indication = null, role = Role.Button, onClick = onClick)
.testTag("glasses-button"),
) {
Icon(GlassesIcon, stringResource(R.string.glasses_open), tint = line, modifier = Modifier.size(22.dp))
}
}
}
@Composable
internal fun GlassesPlayingHint(model: String) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(GlassesIcon, null, tint = Color.White.copy(alpha = 0.35f), modifier = Modifier.size(56.dp))
Spacer(Modifier.height(10.dp))
Text(stringResource(R.string.glasses_playing, model), color = Color.White.copy(alpha = 0.45f), fontSize = 15.sp)
}
}
}
@@ -23,6 +23,7 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -210,6 +211,8 @@ internal fun LiftHero(
onOpenChange: (Boolean) -> Unit = {},
start: Dp = 44.dp,
bottom: Dp = 44.dp,
drop: DropSheetState? = null,
overlay: @Composable BoxWithConstraintsScope.(lifted: () -> Float) -> Unit = {},
) {
val density = LocalDensity.current
val inputMode = LocalInputModeManager.current
@@ -455,19 +458,36 @@ internal fun LiftHero(
// A swipe anywhere on the hero moves the sheet under the finger - up from rest, down
// when open - and lets go open or shut by how far it went and how fast. A swipe that
// starts the wrong way (down at rest) is left alone, for the page under it.
.pointerInput(Unit) {
.pointerInput(drop) {
val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
var decided = false
val dropFlick = 600.dp.toPx()
detectVerticalDragGestures(
onDragStart = { tracker.resetTracking(); decided = false; drag.engaged = false },
onDragEnd = { if (drag.engaged) settleDrag(tracker.calculateVelocity().y) },
onDragCancel = { if (drag.engaged) settleDrag(0f) },
onDragEnd = {
if (drag.engaged) settleDrag(tracker.calculateVelocity().y)
else if (drop?.engaged == true) drop.settle(tracker.calculateVelocity().y, dropFlick)
},
onDragCancel = {
if (drag.engaged) settleDrag(0f)
else if (drop?.engaged == true) drop.settle(0f, dropFlick)
},
) { change, amount ->
if (launching) return@detectVerticalDragGestures
if (!decided) {
decided = true
// Up at rest opens; down when open closes. Anything else is not for the sheet.
if (if (open) amount > 0f else amount < 0f) beginDrag()
when {
drop != null && (drop.open || drop.shown) -> if (drop.open && amount < 0f) drop.beginDrag()
open || shown || amount < 0f -> if (if (open) amount > 0f else amount < 0f) beginDrag()
drop != null && amount > 0f -> drop.beginDrag()
}
}
if (drop?.engaged == true) {
change.consume()
tracker.addPosition(change.uptimeMillis, change.position)
drop.drag(amount)
return@detectVerticalDragGestures
}
if (!drag.engaged) return@detectVerticalDragGestures
change.consume()
@@ -488,7 +508,8 @@ internal fun LiftHero(
// The wall behind sinks back about the pane's centre.
val foot = h
Box(Modifier.fillMaxSize().clipToBounds().graphicsLayer { val k = 1f - 0.05f * sink.value; scaleX = k; scaleY = k }) { background() }
val wallSink = { maxOf(sink.value, drop?.sink?.value ?: 0f) }
Box(Modifier.fillMaxSize().clipToBounds().graphicsLayer { val k = 1f - 0.05f * wallSink(); scaleX = k; scaleY = k }) { background() }
// The fades stay put: scaled with the wall they stopped short of the pane's edges and left
// a bright band of unfaded wall down the side and along the top.
scrim { sink.value }
@@ -627,7 +648,7 @@ internal fun LiftHero(
// The words and the button, where they always were; the button rides the sheet's edge.
Column(Modifier.align(Alignment.BottomStart).padding(start = start, bottom = bottom, end = 20.dp)) {
Box(Modifier.graphicsLayer { alpha = 1f - sink.value }) { words() }
Box(Modifier.graphicsLayer { alpha = 1f - wallSink() }) { words() }
Spacer(Modifier.height(18.dp))
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Box(
@@ -662,7 +683,7 @@ internal fun LiftHero(
val besideShown = !(open || shown)
Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.graphicsLayer { alpha = 1f - sink.value }
modifier = Modifier.graphicsLayer { alpha = 1f - wallSink() }
.focusProperties { canFocus = besideShown },
) { if (besideShown || sink.value < 0.99f) beside() }
}
@@ -694,6 +715,7 @@ internal fun LiftHero(
}
}
}
overlay { sink.value }
}
}
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.ui
/** Stable page identities keep focus and scroll positions when a secondary display comes and goes. */
internal enum class SessionDrawerPage(val firstControl: String) {
CONTROLLER("touch"),
GLASSES("glasses-game"),
SECOND_SCREEN("second-screen:none"),
DISPLAY("hud"),
EFFECTS("look"),
@@ -10,8 +11,10 @@ internal enum class SessionDrawerPage(val firstControl: String) {
SESSION("suspend"),
}
internal fun sessionDrawerPages(hasSecondScreen: Boolean): List<SessionDrawerPage> =
SessionDrawerPage.entries.filter { hasSecondScreen || it != SessionDrawerPage.SECOND_SCREEN }
internal fun sessionDrawerPages(hasSecondScreen: Boolean, hasGlasses: Boolean = false): List<SessionDrawerPage> =
SessionDrawerPage.entries.filter {
(hasSecondScreen || it != SessionDrawerPage.SECOND_SCREEN) && (hasGlasses || it != SessionDrawerPage.GLASSES)
}
internal fun List<SessionDrawerPage>.step(current: SessionDrawerPage, direction: Int): SessionDrawerPage {
val index = indexOf(current).coerceAtLeast(0)
@@ -149,6 +149,7 @@ import java.util.Locale
private val SessionDrawerPage.title: Int get() = when (this) {
SessionDrawerPage.CONTROLLER -> R.string.drawer_page_controller
SessionDrawerPage.GLASSES -> R.string.drawer_page_glasses
SessionDrawerPage.SECOND_SCREEN -> R.string.drawer_second_screen
SessionDrawerPage.DISPLAY -> R.string.drawer_page_display
SessionDrawerPage.EFFECTS -> R.string.drawer_page_effects
@@ -158,6 +159,7 @@ private val SessionDrawerPage.title: Int get() = when (this) {
private val SessionDrawerPage.icon get() = when (this) {
SessionDrawerPage.CONTROLLER -> Icons.Outlined.SportsEsports
SessionDrawerPage.GLASSES -> GlassesIcon
SessionDrawerPage.SECOND_SCREEN -> Icons.Outlined.ScreenShare
SessionDrawerPage.DISPLAY -> Icons.Outlined.DesktopWindows
SessionDrawerPage.EFFECTS -> Icons.Outlined.AutoFixHigh
@@ -217,6 +219,8 @@ class DrawerActions(
val cardboardSensor: Boolean = false,
/** SteamVR in the guest for Cardboard mode (gpu/SteamVr); null while unknown. */
val steamVr: SteamVr.Status? = null,
val glassesPresent: Boolean = false,
val glassesOutput: Boolean = false,
/** Texture filtering for DirectX 9-11 games (core/TextureFiltering); lands on their next launch. */
val textureAnisotropy: Int? = 0,
val textureLodBias: String? = TextureFiltering.LOD_BIAS_OFF,
@@ -254,6 +258,7 @@ class DrawerActions(
val onEffects: (ScreenEffects) -> Unit = {},
val onCardboard: (Cardboard) -> Unit = {},
val onCardboardRecenter: () -> Unit = {},
val onGlassesRecenter: () -> Unit = {},
/** install, start or stop SteamVR. */
val onSteamVr: (String) -> Unit = {},
val onTextureAnisotropy: (Int?) -> Unit = {},
@@ -309,7 +314,7 @@ class DrawerActions(
@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, controllerActive: Boolean, onPageChange: (SessionDrawerPage) -> Unit, a: DrawerActions) {
val pages = sessionDrawerPages(a.steam && a.secondScreenDisplays.isNotEmpty())
val pages = sessionDrawerPages(a.steam && a.secondScreenDisplays.isNotEmpty(), a.glassesPresent)
val page = requestedPage.takeIf { it in pages } ?: SessionDrawerPage.CONTROLLER
LaunchedEffect(requestedPage, pages) { if (page != requestedPage) onPageChange(page) }
val colors = MaterialTheme.colorScheme
@@ -477,7 +482,7 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
stringResource(R.string.display_sharpness), null, a.upscaleSharpness, 0..100, step = 5,
format = { "$it%" }, modifier = focus.track(page, "upscale-sharpness"), onChange = a.onUpscaleSharpness)
}
CardboardGroup(host, a) { key -> focus.track(page, key) }
if (!a.glassesOutput) CardboardGroup(host, a) { key -> focus.track(page, key) }
}
SessionDrawerPage.EFFECTS -> {
ScreenEffectsGroup(host, a, advancedEffectsExpanded,
@@ -541,6 +546,11 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
SessionDrawerPage.SECOND_SCREEN -> {
SecondScreenGroup(host, a) { key -> focus.track(page, key) }
}
SessionDrawerPage.GLASSES -> {
val (g, s) = rememberGlasses()
GlassesGroups(host, g, s, { key -> focus.track(page, key) }, onRecenter = a.onGlassesRecenter,
steamVr = { SteamVrRow(a) })
}
SessionDrawerPage.GAMES -> {
if (a.steam) SelectedGameProfileGroup(host, a) { key -> focus.track(page, key) }
Text(stringResource(R.string.fex_next_launch), fontSize = 12.sp,
@@ -948,18 +958,23 @@ private fun CardboardGroup(host: MenuHost, a: DrawerActions, track: (String) ->
}
if (c.headTracking && a.cardboardSensor)
ActionRow(stringResource(R.string.cardboard_recenter), null, stringResource(R.string.cardboard_recenter_button), a.onCardboardRecenter)
val vrLabel = stringResource(R.string.cardboard_steamvr)
when (a.steamVr) {
SteamVr.Status.NOT_INSTALLED -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_install_hint),
stringResource(R.string.cardboard_steamvr_install), { a.onSteamVr("install") })
SteamVr.Status.INSTALLING -> SettingsRow(vrLabel, stringResource(R.string.cardboard_steamvr_installing)) {}
SteamVr.Status.READY -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_ready_hint),
stringResource(R.string.cardboard_steamvr_start), { a.onSteamVr("start") })
SteamVr.Status.RUNNING -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_running_hint),
stringResource(R.string.cardboard_steamvr_stop), { a.onSteamVr("stop") })
SteamVr.Status.UNAVAILABLE -> SettingsRow(vrLabel, stringResource(R.string.cardboard_steamvr_unavailable)) {}
null -> {}
}
SteamVrRow(a)
}
}
@Composable
private fun SteamVrRow(a: DrawerActions) {
val vrLabel = stringResource(R.string.cardboard_steamvr)
when (a.steamVr) {
SteamVr.Status.NOT_INSTALLED -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_install_hint),
stringResource(R.string.cardboard_steamvr_install), { a.onSteamVr("install") })
SteamVr.Status.INSTALLING -> SettingsRow(vrLabel, stringResource(R.string.cardboard_steamvr_installing)) {}
SteamVr.Status.READY -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_ready_hint),
stringResource(R.string.cardboard_steamvr_start), { a.onSteamVr("start") })
SteamVr.Status.RUNNING -> ActionRow(vrLabel, stringResource(R.string.cardboard_steamvr_running_hint),
stringResource(R.string.cardboard_steamvr_stop), { a.onSteamVr("stop") })
SteamVr.Status.UNAVAILABLE -> SettingsRow(vrLabel, stringResource(R.string.cardboard_steamvr_unavailable)) {}
null -> {}
}
}
@@ -103,8 +103,18 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier)
}
},
)
val (glasses, glassesSettings) = rememberGlasses()
val drop = rememberDropSheetState()
LaunchedEffect(glasses.present) { if (!glasses.present && (drop.open || drop.shown)) drop.reset() }
val glassesLabel = stringResource(R.string.glasses_open)
LiftHero(
key = "steam", tabs = tabs,
drop = drop.takeIf { glasses.present },
overlay = { lifted ->
if (glasses.present) DropSheet(drop, glasses.model, GlassesIcon, glassesLabel, covered = lifted) {
GlassesGroups(rememberMenuHost(), glasses, glassesSettings, inSession = false)
}
},
onOpen = a.onSteamSettings, onPrimary = a.onPlay,
modifier = modifier.clipToBounds(),
start = if (narrow) 20.dp else 44.dp, bottom = if (narrow) 20.dp else 44.dp,
@@ -455,7 +455,7 @@ public final class WaylandCompositor {
* stereo pair. Distances in metres, field of view in degrees. */
public static native void nativeSetCardboard(boolean enabled, int source, float ipdM, float screenDistM,
float screenWidthM, float k1, float k2, float fovDeg,
float lensInset, float chroma);
float lensInset, float chroma, boolean mono, float displayHz);
/** The head pose for Cardboard mode: head -> world rotation, row-major 3x3 (any thread). {@code live}
* = a sensor produced it. */
+37
View File
@@ -2147,4 +2147,41 @@
<string name="achievements_none">%1$s no indica logros para este juego.</string>
<string name="achievements_show_all">Ver los %1$d</string>
<string name="achievements_dialog_title">Logros · %1$s</string>
<string name="drawer_page_glasses">Gafas</string>
<string name="glasses_open">Gafas VITURE</string>
<string name="glasses_control">Control de las gafas</string>
<string name="glasses_allow">Permitir</string>
<string name="glasses_allow_hint">Permite que DroidDeck ajuste la frecuencia de actualización, el modo 3D, el brillo, el volumen y el bloqueo solar de las gafas, y lea su seguimiento de cabeza.</string>
<string name="glasses_asking">Esperando a que lo permitas en el aviso de Android…</string>
<string name="glasses_denied">Se rechazó el control. Permítelo para cambiar los ajustes de las gafas.</string>
<string name="glasses_no_control">Estas gafas muestran el juego, pero sus ajustes no se pueden cambiar desde aquí.</string>
<string name="glasses_output">Salida</string>
<string name="glasses_game_on">Juego en las gafas</string>
<string name="glasses_game_on_hint">Muestra el juego solo en las gafas. Desactivado: el teléfono lo muestra y Android duplica la pantalla del teléfono.</string>
<string name="glasses_display">Pantalla</string>
<string name="glasses_refresh">Frecuencia de actualización</string>
<string name="glasses_3d">3D (lado a lado)</string>
<string name="glasses_3d_hint">Cada ojo recibe su propia mitad de una imagen de 3840×1080. Si la imagen se pierde, mantén pulsado el botón 2D/3D de las gafas.</string>
<string name="glasses_3d_unsupported">Este teléfono no puede enviar la señal de 3840×1080 que necesita el 3D. Los juegos de RV muestran la vista de un ojo, con seguimiento de cabeza.</string>
<string name="glasses_no_signal">Sin imagen: las gafas están en un modo que este teléfono no puede enviar. Mantén pulsado su botón 2D/3D o desconéctalas y vuelve a conectarlas.</string>
<string name="glasses_3d_refresh">Frecuencia en 3D</string>
<string name="glasses_auto3d">3D para juegos de RV</string>
<string name="glasses_auto3d_hint">Cambia a 3D mientras SteamVR está en marcha, y vuelve después.</string>
<string name="glasses_picture">Imagen 3D</string>
<string name="glasses_picture_hint">Automático: SteamVR llena la vista; los demás juegos flotan como una pantalla en 3D y se ven planos en 2D.</string>
<string name="glasses_picture_auto">Automático</string>
<string name="glasses_picture_screen">Pantalla virtual</string>
<string name="glasses_picture_sbs">Juego lado a lado</string>
<string name="glasses_screen_size">Ancho de la pantalla</string>
<string name="glasses_screen_distance">Distancia de la pantalla</string>
<string name="glasses_hardware">Gafas</string>
<string name="glasses_brightness">Brillo</string>
<string name="glasses_volume">Volumen</string>
<string name="glasses_sunblock">Bloqueo solar</string>
<string name="glasses_sunblock_hint">Oscurece las lentes frontales.</string>
<string name="glasses_tracking">Seguimiento de cabeza</string>
<string name="glasses_head_tracking">Seguimiento de cabeza</string>
<string name="glasses_head_tracking_hint">Los sensores de las gafas giran la pantalla virtual y SteamVR con tu cabeza.</string>
<string name="glasses_recenter">Recentrar la vista</string>
<string name="glasses_playing">Jugando en %1$s</string>
</resources>
+37
View File
@@ -977,4 +977,41 @@
<string name="achievements_none">%1$s n\'indique aucun succès pour ce jeu.</string>
<string name="achievements_show_all">Tout afficher (%1$d)</string>
<string name="achievements_dialog_title">Succès · %1$s</string>
<string name="drawer_page_glasses">Lunettes</string>
<string name="glasses_open">Lunettes VITURE</string>
<string name="glasses_control">Contrôle des lunettes</string>
<string name="glasses_allow">Autoriser</string>
<string name="glasses_allow_hint">Permet à DroidDeck de régler la fréquence d\'affichage, le mode 3D, la luminosité, le volume et le pare-soleil des lunettes, et de lire leur suivi de la tête.</string>
<string name="glasses_asking">En attente de votre autorisation dans la fenêtre d\'Android…</string>
<string name="glasses_denied">Le contrôle a été refusé. Autorisez-le pour modifier les réglages des lunettes.</string>
<string name="glasses_no_control">Ces lunettes affichent le jeu, mais leurs réglages ne peuvent pas être modifiés d\'ici.</string>
<string name="glasses_output">Sortie</string>
<string name="glasses_game_on">Jeu sur les lunettes</string>
<string name="glasses_game_on_hint">Affiche le jeu uniquement sur les lunettes. Désactivé : le téléphone l\'affiche et Android duplique l\'écran du téléphone.</string>
<string name="glasses_display">Affichage</string>
<string name="glasses_refresh">Fréquence d\'affichage</string>
<string name="glasses_3d">3D (côte à côte)</string>
<string name="glasses_3d_hint">Chaque œil reçoit sa propre moitié d\'une image de 3840×1080. Si l\'image disparaît, maintenez le bouton 2D/3D des lunettes.</string>
<string name="glasses_3d_unsupported">Ce téléphone ne peut pas envoyer le signal 3840×1080 nécessaire à la 3D. Les jeux VR affichent la vue d\'un seul œil, avec le suivi de la tête.</string>
<string name="glasses_no_signal">Pas d\'image : les lunettes sont dans un mode que ce téléphone ne peut pas envoyer. Maintenez leur bouton 2D/3D, ou débranchez-les puis rebranchez-les.</string>
<string name="glasses_3d_refresh">Fréquence en 3D</string>
<string name="glasses_auto3d">3D pour les jeux VR</string>
<string name="glasses_auto3d_hint">Passe en 3D pendant que SteamVR tourne, puis revient ensuite.</string>
<string name="glasses_picture">Image 3D</string>
<string name="glasses_picture_hint">Automatique : SteamVR remplit la vue ; les autres jeux flottent comme un écran en 3D et restent plats en 2D.</string>
<string name="glasses_picture_auto">Automatique</string>
<string name="glasses_picture_screen">Écran virtuel</string>
<string name="glasses_picture_sbs">Jeu côte à côte</string>
<string name="glasses_screen_size">Largeur de l\'écran</string>
<string name="glasses_screen_distance">Distance de l\'écran</string>
<string name="glasses_hardware">Lunettes</string>
<string name="glasses_brightness">Luminosité</string>
<string name="glasses_volume">Volume</string>
<string name="glasses_sunblock">Pare-soleil</string>
<string name="glasses_sunblock_hint">Assombrit les verres avant.</string>
<string name="glasses_tracking">Suivi de la tête</string>
<string name="glasses_head_tracking">Suivi de la tête</string>
<string name="glasses_head_tracking_hint">Les capteurs des lunettes font tourner l\'écran virtuel et SteamVR avec votre tête.</string>
<string name="glasses_recenter">Recentrer la vue</string>
<string name="glasses_playing">En jeu sur %1$s</string>
</resources>
+37
View File
@@ -2103,4 +2103,41 @@
<string name="achievements_none">%1$s にはこのゲームの実績がありません。</string>
<string name="achievements_show_all">すべて表示(%1$d)</string>
<string name="achievements_dialog_title">実績 · %1$s</string>
<string name="drawer_page_glasses">グラス</string>
<string name="glasses_open">VITURE グラス</string>
<string name="glasses_control">グラスの制御</string>
<string name="glasses_allow">許可</string>
<string name="glasses_allow_hint">DroidDeck がグラスのリフレッシュレート、3D モード、明るさ、音量、サンブロックを設定し、ヘッドトラッキングを読み取れるようにします。</string>
<string name="glasses_asking">Android のダイアログで許可されるのを待っています…</string>
<string name="glasses_denied">制御が拒否されました。グラスの設定を変更するには許可してください。</string>
<string name="glasses_no_control">このグラスにはゲームが表示されますが、ここから設定は変更できません。</string>
<string name="glasses_output">出力</string>
<string name="glasses_game_on">グラスにゲームを表示</string>
<string name="glasses_game_on_hint">ゲームをグラスだけに表示します。オフ: スマートフォンに表示し、Android がスマートフォンの画面をミラーリングします。</string>
<string name="glasses_display">表示</string>
<string name="glasses_refresh">リフレッシュレート</string>
<string name="glasses_3d">3D(サイドバイサイド)</string>
<string name="glasses_3d_hint">左右の目に 3840×1080 の画像の半分ずつを表示します。映像が消えたら、グラスの 2D/3D ボタンを長押ししてください。</string>
<string name="glasses_3d_unsupported">このスマートフォンは 3D に必要な 3840×1080 の信号を出力できません。VR ゲームはヘッドトラッキング付きで片目の映像を表示します。</string>
<string name="glasses_no_signal">映像なし: グラスがこのスマートフォンで出力できないモードになっています。2D/3D ボタンを長押しするか、抜き差ししてください。</string>
<string name="glasses_3d_refresh">3D のリフレッシュレート</string>
<string name="glasses_auto3d">VR ゲームで 3D</string>
<string name="glasses_auto3d_hint">SteamVR の実行中は 3D に切り替え、終了後に戻します。</string>
<string name="glasses_picture">3D の映像</string>
<string name="glasses_picture_hint">自動: SteamVR は視界全体に表示し、ほかのゲームは 3D ではスクリーンとして浮かび、2D では平面のままです。</string>
<string name="glasses_picture_auto">自動</string>
<string name="glasses_picture_screen">仮想スクリーン</string>
<string name="glasses_picture_sbs">サイドバイサイドのゲーム</string>
<string name="glasses_screen_size">スクリーンの幅</string>
<string name="glasses_screen_distance">スクリーンまでの距離</string>
<string name="glasses_hardware">グラス</string>
<string name="glasses_brightness">明るさ</string>
<string name="glasses_volume">音量</string>
<string name="glasses_sunblock">サンブロック</string>
<string name="glasses_sunblock_hint">前面のレンズを暗くします。</string>
<string name="glasses_tracking">ヘッドトラッキング</string>
<string name="glasses_head_tracking">ヘッドトラッキング</string>
<string name="glasses_head_tracking_hint">グラスのセンサーで、頭の動きに合わせて仮想スクリーンと SteamVR を動かします。</string>
<string name="glasses_recenter">視点をリセット</string>
<string name="glasses_playing">%1$s でプレイ中</string>
</resources>
+37
View File
@@ -2103,4 +2103,41 @@
<string name="achievements_none">%1$s에 이 게임의 도전 과제가 없습니다.</string>
<string name="achievements_show_all">모두 보기(%1$d)</string>
<string name="achievements_dialog_title">도전 과제 · %1$s</string>
<string name="drawer_page_glasses">안경</string>
<string name="glasses_open">VITURE 안경</string>
<string name="glasses_control">안경 제어</string>
<string name="glasses_allow">허용</string>
<string name="glasses_allow_hint">DroidDeck이 안경의 주사율, 3D 모드, 밝기, 볼륨, 선블록을 설정하고 헤드 트래킹을 읽도록 허용합니다.</string>
<string name="glasses_asking">Android 안내 창에서 허용하기를 기다리는 중…</string>
<string name="glasses_denied">제어가 거부되었습니다. 안경 설정을 바꾸려면 허용하세요.</string>
<string name="glasses_no_control">이 안경은 게임을 표시하지만 여기에서 설정을 바꿀 수는 없습니다.</string>
<string name="glasses_output">출력</string>
<string name="glasses_game_on">안경에 게임 표시</string>
<string name="glasses_game_on_hint">게임을 안경에만 표시합니다. 끄면 휴대폰에 표시되고 Android가 휴대폰 화면을 미러링합니다.</string>
<string name="glasses_display">디스플레이</string>
<string name="glasses_refresh">주사율</string>
<string name="glasses_3d">3D (사이드 바이 사이드)</string>
<string name="glasses_3d_hint">각 눈이 3840×1080 화면의 절반씩을 봅니다. 화면이 끊기면 안경의 2D/3D 버튼을 길게 누르세요.</string>
<string name="glasses_3d_unsupported">이 휴대폰은 3D에 필요한 3840×1080 신호를 보낼 수 없습니다. VR 게임은 헤드 트래킹과 함께 한쪽 눈의 화면을 보여 줍니다.</string>
<string name="glasses_no_signal">화면 없음: 안경이 이 휴대폰에서 보낼 수 없는 모드에 있습니다. 2D/3D 버튼을 길게 누르거나 안경을 뺐다가 다시 연결하세요.</string>
<string name="glasses_3d_refresh">3D 주사율</string>
<string name="glasses_auto3d">VR 게임에서 3D</string>
<string name="glasses_auto3d_hint">SteamVR이 실행되는 동안 3D로 전환하고 끝나면 되돌립니다.</string>
<string name="glasses_picture">3D 화면</string>
<string name="glasses_picture_hint">자동: SteamVR은 시야 전체를 채우고, 다른 게임은 3D에서는 화면으로 떠 있고 2D에서는 평면으로 보입니다.</string>
<string name="glasses_picture_auto">자동</string>
<string name="glasses_picture_screen">가상 화면</string>
<string name="glasses_picture_sbs">사이드 바이 사이드 게임</string>
<string name="glasses_screen_size">화면 너비</string>
<string name="glasses_screen_distance">화면 거리</string>
<string name="glasses_hardware">안경</string>
<string name="glasses_brightness">밝기</string>
<string name="glasses_volume">볼륨</string>
<string name="glasses_sunblock">선블록</string>
<string name="glasses_sunblock_hint">앞쪽 렌즈를 어둡게 합니다.</string>
<string name="glasses_tracking">헤드 트래킹</string>
<string name="glasses_head_tracking">헤드 트래킹</string>
<string name="glasses_head_tracking_hint">안경의 센서가 머리 움직임에 맞춰 가상 화면과 SteamVR을 돌립니다.</string>
<string name="glasses_recenter">시점 재설정</string>
<string name="glasses_playing">%1$s에서 플레이 중</string>
</resources>
+37
View File
@@ -1088,4 +1088,41 @@
<string name="achievements_none">В %1$s нет достижений для этой игры.</string>
<string name="achievements_show_all">Показать все (%1$d)</string>
<string name="achievements_dialog_title">Достижения · %1$s</string>
<string name="drawer_page_glasses">Очки</string>
<string name="glasses_open">Очки VITURE</string>
<string name="glasses_control">Управление очками</string>
<string name="glasses_allow">Разрешить</string>
<string name="glasses_allow_hint">Разрешите DroidDeck менять частоту обновления, режим 3D, яркость, громкость и затемнение очков и считывать отслеживание головы.</string>
<string name="glasses_asking">Ожидание разрешения в окне Android…</string>
<string name="glasses_denied">Управление отклонено. Разрешите его, чтобы менять настройки очков.</string>
<string name="glasses_no_control">Эти очки показывают игру, но их настройки нельзя изменить отсюда.</string>
<string name="glasses_output">Вывод</string>
<string name="glasses_game_on">Игра на очках</string>
<string name="glasses_game_on_hint">Показывать игру только на очках. Выкл.: игру показывает телефон, а Android дублирует экран телефона.</string>
<string name="glasses_display">Экран</string>
<string name="glasses_refresh">Частота обновления</string>
<string name="glasses_3d">3D (стереопара)</string>
<string name="glasses_3d_hint">Каждый глаз получает свою половину изображения 3840×1080. Если изображение пропадёт, удерживайте кнопку 2D/3D на очках.</string>
<string name="glasses_3d_unsupported">Этот телефон не может передать сигнал 3840×1080, нужный для 3D. VR-игры показывают вид одного глаза с отслеживанием головы.</string>
<string name="glasses_no_signal">Нет изображения: очки в режиме, который этот телефон не может передать. Удерживайте их кнопку 2D/3D или отключите и снова подключите очки.</string>
<string name="glasses_3d_refresh">Частота в 3D</string>
<string name="glasses_auto3d">3D для VR-игр</string>
<string name="glasses_auto3d_hint">Включает 3D, пока работает SteamVR, и возвращает обратно после.</string>
<string name="glasses_picture">Изображение 3D</string>
<string name="glasses_picture_hint">Автоматически: SteamVR заполняет весь обзор; другие игры висят экраном в 3D и остаются плоскими в 2D.</string>
<string name="glasses_picture_auto">Автоматически</string>
<string name="glasses_picture_screen">Виртуальный экран</string>
<string name="glasses_picture_sbs">Игра в стереопаре</string>
<string name="glasses_screen_size">Ширина экрана</string>
<string name="glasses_screen_distance">Расстояние до экрана</string>
<string name="glasses_hardware">Очки</string>
<string name="glasses_brightness">Яркость</string>
<string name="glasses_volume">Громкость</string>
<string name="glasses_sunblock">Затемнение</string>
<string name="glasses_sunblock_hint">Затемняет передние линзы.</string>
<string name="glasses_tracking">Отслеживание головы</string>
<string name="glasses_head_tracking">Отслеживание головы</string>
<string name="glasses_head_tracking_hint">Датчики очков поворачивают виртуальный экран и SteamVR вслед за головой.</string>
<string name="glasses_recenter">Сбросить вид</string>
<string name="glasses_playing">Игра на %1$s</string>
</resources>
@@ -2103,4 +2103,41 @@
<string name="achievements_none">%1$s 未列出此游戏的成就。</string>
<string name="achievements_show_all">显示全部 %1$d 项</string>
<string name="achievements_dialog_title">成就 · %1$s</string>
<string name="drawer_page_glasses">眼镜</string>
<string name="glasses_open">VITURE 眼镜</string>
<string name="glasses_control">眼镜控制</string>
<string name="glasses_allow">允许</string>
<string name="glasses_allow_hint">允许 DroidDeck 设置眼镜的刷新率、3D 模式、亮度、音量和遮光,并读取其头部追踪。</string>
<string name="glasses_asking">正在等待你在 Android 提示中允许…</string>
<string name="glasses_denied">控制已被拒绝。允许后才能更改眼镜设置。</string>
<string name="glasses_no_control">这副眼镜可以显示游戏,但无法在此更改其设置。</string>
<string name="glasses_output">输出</string>
<string name="glasses_game_on">在眼镜上显示游戏</string>
<string name="glasses_game_on_hint">只在眼镜上显示游戏。关闭:由手机显示,Android 会镜像手机画面。</string>
<string name="glasses_display">显示</string>
<string name="glasses_refresh">刷新率</string>
<string name="glasses_3d">3D(左右分屏)</string>
<string name="glasses_3d_hint">每只眼睛各看 3840×1080 画面的一半。如果画面消失,请长按眼镜上的 2D/3D 按钮。</string>
<string name="glasses_3d_unsupported">这部手机无法输出 3D 所需的 3840×1080 信号。VR 游戏会显示单眼画面,并带有头部追踪。</string>
<string name="glasses_no_signal">无画面:眼镜处于这部手机无法输出的模式。请长按眼镜的 2D/3D 按钮,或拔下后重新连接。</string>
<string name="glasses_3d_refresh">3D 刷新率</string>
<string name="glasses_auto3d">VR 游戏使用 3D</string>
<string name="glasses_auto3d_hint">SteamVR 运行时切换到 3D,结束后切回。</string>
<string name="glasses_picture">3D 画面</string>
<string name="glasses_picture_hint">自动:SteamVR 填满视野;其他游戏在 3D 下以屏幕形式悬浮,在 2D 下保持平面。</string>
<string name="glasses_picture_auto">自动</string>
<string name="glasses_picture_screen">虚拟屏幕</string>
<string name="glasses_picture_sbs">左右分屏游戏</string>
<string name="glasses_screen_size">屏幕宽度</string>
<string name="glasses_screen_distance">屏幕距离</string>
<string name="glasses_hardware">眼镜</string>
<string name="glasses_brightness">亮度</string>
<string name="glasses_volume">音量</string>
<string name="glasses_sunblock">遮光</string>
<string name="glasses_sunblock_hint">让前镜片变暗。</string>
<string name="glasses_tracking">头部追踪</string>
<string name="glasses_head_tracking">头部追踪</string>
<string name="glasses_head_tracking_hint">眼镜自带的传感器会随头部转动虚拟屏幕和 SteamVR。</string>
<string name="glasses_recenter">重新居中视角</string>
<string name="glasses_playing">正在 %1$s 上游玩</string>
</resources>
@@ -2103,4 +2103,41 @@
<string name="achievements_none">%1$s 未列出此遊戲的成就。</string>
<string name="achievements_show_all">顯示全部 %1$d 項</string>
<string name="achievements_dialog_title">成就 · %1$s</string>
<string name="drawer_page_glasses">眼鏡</string>
<string name="glasses_open">VITURE 眼鏡</string>
<string name="glasses_control">眼鏡控制</string>
<string name="glasses_allow">允許</string>
<string name="glasses_allow_hint">允許 DroidDeck 設定眼鏡的更新率、3D 模式、亮度、音量和遮光,並讀取其頭部追蹤。</string>
<string name="glasses_asking">正在等候你在 Android 提示中允許…</string>
<string name="glasses_denied">控制已被拒絕。允許後才可更改眼鏡設定。</string>
<string name="glasses_no_control">這副眼鏡可以顯示遊戲,但無法在此更改其設定。</string>
<string name="glasses_output">輸出</string>
<string name="glasses_game_on">在眼鏡上顯示遊戲</string>
<string name="glasses_game_on_hint">只在眼鏡上顯示遊戲。關閉:由手機顯示,Android 會鏡像手機畫面。</string>
<string name="glasses_display">顯示</string>
<string name="glasses_refresh">更新率</string>
<string name="glasses_3d">3D(左右並排)</string>
<string name="glasses_3d_hint">每隻眼睛各看 3840×1080 畫面的一半。如果畫面消失,請長按眼鏡上的 2D/3D 按鈕。</string>
<string name="glasses_3d_unsupported">這部手機無法輸出 3D 所需的 3840×1080 訊號。VR 遊戲會顯示單眼畫面,並配合頭部追蹤。</string>
<string name="glasses_no_signal">沒有畫面:眼鏡處於這部手機無法輸出的模式。請長按眼鏡的 2D/3D 按鈕,或拔除後重新連接。</string>
<string name="glasses_3d_refresh">3D 更新率</string>
<string name="glasses_auto3d">VR 遊戲使用 3D</string>
<string name="glasses_auto3d_hint">SteamVR 運行時切換到 3D,結束後切回。</string>
<string name="glasses_picture">3D 畫面</string>
<string name="glasses_picture_hint">自動:SteamVR 填滿視野;其他遊戲在 3D 下以螢幕形式懸浮,在 2D 下保持平面。</string>
<string name="glasses_picture_auto">自動</string>
<string name="glasses_picture_screen">虛擬螢幕</string>
<string name="glasses_picture_sbs">左右並排遊戲</string>
<string name="glasses_screen_size">螢幕寬度</string>
<string name="glasses_screen_distance">螢幕距離</string>
<string name="glasses_hardware">眼鏡</string>
<string name="glasses_brightness">亮度</string>
<string name="glasses_volume">音量</string>
<string name="glasses_sunblock">遮光</string>
<string name="glasses_sunblock_hint">令前鏡片變暗。</string>
<string name="glasses_tracking">頭部追蹤</string>
<string name="glasses_head_tracking">頭部追蹤</string>
<string name="glasses_head_tracking_hint">眼鏡自帶的感應器會隨頭部轉動虛擬螢幕和 SteamVR。</string>
<string name="glasses_recenter">重新置中視角</string>
<string name="glasses_playing">正在 %1$s 上遊玩</string>
</resources>
@@ -2103,4 +2103,41 @@
<string name="achievements_none">%1$s 沒有列出這款遊戲的成就。</string>
<string name="achievements_show_all">顯示全部 %1$d 項</string>
<string name="achievements_dialog_title">成就 · %1$s</string>
<string name="drawer_page_glasses">眼鏡</string>
<string name="glasses_open">VITURE 眼鏡</string>
<string name="glasses_control">眼鏡控制</string>
<string name="glasses_allow">允許</string>
<string name="glasses_allow_hint">允許 DroidDeck 設定眼鏡的更新率、3D 模式、亮度、音量和遮光,並讀取其頭部追蹤。</string>
<string name="glasses_asking">正在等待你在 Android 提示中允許…</string>
<string name="glasses_denied">控制已被拒絕。允許後才能變更眼鏡設定。</string>
<string name="glasses_no_control">這副眼鏡可以顯示遊戲,但無法在此變更其設定。</string>
<string name="glasses_output">輸出</string>
<string name="glasses_game_on">在眼鏡上顯示遊戲</string>
<string name="glasses_game_on_hint">只在眼鏡上顯示遊戲。關閉:由手機顯示,Android 會鏡像手機畫面。</string>
<string name="glasses_display">顯示</string>
<string name="glasses_refresh">更新率</string>
<string name="glasses_3d">3D(左右並排)</string>
<string name="glasses_3d_hint">每隻眼睛各看 3840×1080 畫面的一半。如果畫面消失,請長按眼鏡上的 2D/3D 按鈕。</string>
<string name="glasses_3d_unsupported">這支手機無法輸出 3D 所需的 3840×1080 訊號。VR 遊戲會顯示單眼畫面,並搭配頭部追蹤。</string>
<string name="glasses_no_signal">沒有畫面:眼鏡處於這支手機無法輸出的模式。請長按眼鏡的 2D/3D 按鈕,或拔除後重新連接。</string>
<string name="glasses_3d_refresh">3D 更新率</string>
<string name="glasses_auto3d">VR 遊戲使用 3D</string>
<string name="glasses_auto3d_hint">SteamVR 執行時切換到 3D,結束後切回。</string>
<string name="glasses_picture">3D 畫面</string>
<string name="glasses_picture_hint">自動:SteamVR 填滿視野;其他遊戲在 3D 下以螢幕形式懸浮,在 2D 下保持平面。</string>
<string name="glasses_picture_auto">自動</string>
<string name="glasses_picture_screen">虛擬螢幕</string>
<string name="glasses_picture_sbs">左右並排遊戲</string>
<string name="glasses_screen_size">螢幕寬度</string>
<string name="glasses_screen_distance">螢幕距離</string>
<string name="glasses_hardware">眼鏡</string>
<string name="glasses_brightness">亮度</string>
<string name="glasses_volume">音量</string>
<string name="glasses_sunblock">遮光</string>
<string name="glasses_sunblock_hint">讓前鏡片變暗。</string>
<string name="glasses_tracking">頭部追蹤</string>
<string name="glasses_head_tracking">頭部追蹤</string>
<string name="glasses_head_tracking_hint">眼鏡內建的感測器會隨頭部轉動虛擬螢幕和 SteamVR。</string>
<string name="glasses_recenter">重新置中視角</string>
<string name="glasses_playing">正在 %1$s 上遊玩</string>
</resources>
+40
View File
@@ -2346,4 +2346,44 @@
<string name="gog_achievements_enable_store">Enable GOG achievements in Stores settings</string>
<string name="gog_achievements_unavailable">Achievements unavailable; check session logs</string>
<string name="achievement_unlocked">Achievement unlocked</string>
<string name="drawer_page_glasses">Glasses</string>
<string name="glasses_open">VITURE glasses</string>
<string name="glasses_control">Glasses control</string>
<string name="glasses_allow">Allow</string>
<string name="glasses_allow_hint">Let DroidDeck set the glasses\' refresh rate, 3D mode, brightness, volume and sunblock, and read their head tracking.</string>
<string name="glasses_asking">Waiting for you to allow it in Android\'s prompt…</string>
<string name="glasses_denied">Control was declined. Allow it to change the glasses\' settings.</string>
<string name="glasses_no_control">These glasses show the game, but their settings can\'t be changed from here.</string>
<string name="glasses_output">Output</string>
<string name="glasses_status">%1$d×%2$d · %3$d Hz · %4$s</string>
<string name="glasses_mode_2d">2D</string>
<string name="glasses_mode_3d">3D</string>
<string name="glasses_game_on">Game on glasses</string>
<string name="glasses_game_on_hint">Show the game only on the glasses. Off: the phone shows it and Android mirrors the phone.</string>
<string name="glasses_display">Display</string>
<string name="glasses_refresh">Refresh rate</string>
<string name="glasses_3d">3D (side-by-side)</string>
<string name="glasses_3d_hint">Each eye gets its own half of a 3840×1080 picture. If the picture drops out, hold the glasses\' 2D/3D button.</string>
<string name="glasses_3d_unsupported">This phone can\'t send the 3840×1080 signal 3D needs. VR games show one eye\'s view, with head tracking.</string>
<string name="glasses_no_signal">No picture: the glasses are in a mode this phone can\'t send. Hold their 2D/3D button, or unplug and replug them.</string>
<string name="glasses_3d_refresh">3D refresh rate</string>
<string name="glasses_auto3d">3D for VR games</string>
<string name="glasses_auto3d_hint">Switches to 3D while SteamVR runs, and back after.</string>
<string name="glasses_picture">3D picture</string>
<string name="glasses_picture_hint">Automatic: SteamVR fills the view; other games hang as a screen in 3D and stay flat in 2D.</string>
<string name="glasses_picture_auto">Automatic</string>
<string name="glasses_picture_screen">Virtual screen</string>
<string name="glasses_picture_sbs">Side-by-side game</string>
<string name="glasses_screen_size">Screen width</string>
<string name="glasses_screen_distance">Screen distance</string>
<string name="glasses_hardware">Glasses</string>
<string name="glasses_brightness">Brightness</string>
<string name="glasses_volume">Volume</string>
<string name="glasses_sunblock">Sunblock</string>
<string name="glasses_sunblock_hint">Darkens the front lenses.</string>
<string name="glasses_tracking">Head tracking</string>
<string name="glasses_head_tracking">Head tracking</string>
<string name="glasses_head_tracking_hint">The glasses\' own sensors turn the virtual screen and SteamVR with your head.</string>
<string name="glasses_recenter">Recenter view</string>
<string name="glasses_playing">Playing on %1$s</string>
</resources>
@@ -0,0 +1,111 @@
package com.droiddeck.launcher.glasses
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.nio.ByteBuffer
import kotlin.math.cos
import kotlin.math.sin
class VitureProtocolTest {
private fun u8(b: Byte) = b.toInt() and 0xFF
private fun u16(b: ByteArray, at: Int) = u8(b[at]) or (u8(b[at + 1]) shl 8)
@Test fun crcIsXmodem() {
val check = "123456789".toByteArray()
assertEquals(0x31C3, VitureProtocol.crc16(check, 0, check.size))
}
@Test fun packetLayout() {
val p = VitureProtocol.packet(VitureProtocol.MSG_BRIGHTNESS, byteArrayOf(5, 0))
assertEquals(VitureProtocol.PACKET_SIZE, p.size)
assertEquals(0xFF, u8(p[0]))
assertEquals(VitureProtocol.MAGIC_COMMAND, u8(p[1]))
assertEquals(2 + 0x0C, u16(p, 4))
assertEquals(VitureProtocol.MSG_BRIGHTNESS, u16(p, 14))
assertEquals(5, u8(p[18]))
assertEquals(VitureProtocol.crc16(p, 4, u16(p, 4) + 2), u16(p, 2))
for (i in 6 until 14) assertEquals(0, u8(p[i]))
}
@Test fun parseRoundTripsAndChecksCrc() {
val p = VitureProtocol.packet(VitureProtocol.MSG_DISPLAY, byteArrayOf(0x34))
val f = VitureProtocol.parse(p, p.size)
assertNotNull(f)
assertEquals(VitureProtocol.MAGIC_COMMAND, f!!.magic)
assertEquals(VitureProtocol.MSG_DISPLAY, f.msgId)
assertArrayEquals(byteArrayOf(0x34), f.payload)
assertTrue(f.crcOk)
p[18] = 0x31
assertFalse(VitureProtocol.parse(p, p.size)!!.crcOk)
}
@Test fun parseReadsRepliesAndRejectsJunk() {
val reply = VitureProtocol.packet(VitureProtocol.MSG_GET_VOLUME, byteArrayOf(0, 5))
reply[1] = VitureProtocol.MAGIC_REPORT.toByte()
val f = VitureProtocol.parse(reply, reply.size)!!
assertEquals(VitureProtocol.MAGIC_REPORT, f.magic)
assertArrayEquals(byteArrayOf(0, 5), f.payload)
assertNull(VitureProtocol.parse(ByteArray(64), 64))
assertNull(VitureProtocol.parse(reply, 8))
val wrongMagic = reply.copyOf().also { it[1] = 0x10 }
assertNull(VitureProtocol.parse(wrongMagic, wrongMagic.size))
}
@Test fun modelsAndModes() {
val pro = VitureProtocol.model(0x101d, "VITURE Pro XR Glasses")!!
assertEquals("VITURE Pro XR", pro.name)
assertEquals(VitureProtocol.Family.PRO, pro.family)
assertTrue(pro.controllable)
assertFalse(pro.sbs90)
assertEquals(0x34, pro.mode(120, false))
assertEquals(0x33, pro.mode(90, false))
assertEquals(0x31, pro.mode(60, false))
assertEquals(0x32, pro.mode(90, true))
assertEquals(0x32, pro.mode(60, true))
val luma = VitureProtocol.model(0x1131, null)!!
assertEquals("VITURE Luma", luma.name)
assertTrue(luma.sbs90)
assertEquals(0x44, luma.mode(120, false))
assertEquals(0x45, luma.mode(90, true))
assertEquals(0x42, luma.mode(60, true))
assertFalse(VitureProtocol.model(0x1201, null)!!.controllable)
assertNull(VitureProtocol.model(0x9999, "VITURE"))
assertEquals("VITURE One", VitureProtocol.model(0x1011, "Something else")!!.name)
}
@Test fun modeHelpers() {
listOf(0x32, 0x35, 0x42, 0x45).forEach { assertTrue(VitureProtocol.modeIsSbs(it)) }
listOf(0x31, 0x33, 0x34, 0x41, 0x44).forEach { assertFalse(VitureProtocol.modeIsSbs(it)) }
assertEquals(120, VitureProtocol.modeHz(0x34))
assertEquals(90, VitureProtocol.modeHz(0x35))
assertEquals(60, VitureProtocol.modeHz(0x32))
}
@Test fun imuPrefersTheQuaternion() {
val half = Math.toRadians(30.0) / 2
val payload = ByteBuffer.allocate(40)
.putFloat(1f).putFloat(2f).putFloat(3f)
.putFloat(0f).putFloat(0f)
.putFloat(cos(half).toFloat()).putFloat(0f).putFloat(0f).putFloat(sin(half).toFloat())
.array()
val q = VitureProtocol.imuQuaternion(payload)!!
assertArrayEquals(floatArrayOf(0f, 0f, sin(half).toFloat(), cos(half).toFloat()), q, 1e-5f)
}
@Test fun imuFallsBackToEuler() {
val payload = ByteBuffer.allocate(12).putFloat(0f).putFloat(0f).putFloat(30f).array()
val q = VitureProtocol.imuQuaternion(payload)!!
val half = Math.toRadians(30.0) / 2
assertArrayEquals(floatArrayOf(0f, 0f, sin(half).toFloat(), cos(half).toFloat()), q, 1e-5f)
assertNull(VitureProtocol.imuQuaternion(ByteArray(8)))
val nan = ByteBuffer.allocate(12).putFloat(Float.NaN).putFloat(0f).putFloat(0f).array()
assertNull(VitureProtocol.imuQuaternion(nan))
}
}
@@ -174,4 +174,37 @@ class CardboardTest {
assertVec(HeadPose.apply(r, floatArrayOf(0f, 1f, 0f)), HeadPose.apply(s270, floatArrayOf(1f, 0f, 0f)))
assertVec(r, HeadPose.toScreen(HeadPose.toScreen(HeadPose.toScreen(HeadPose.toScreen(r, 1), 1), 1), 1))
}
@Test fun glassesLevelAndAheadIsStraightAhead() {
val s2w = HeadPose.fromGlasses(0f, 0f, 0f, 1f)
assertVec(floatArrayOf(1f, 0f, 0f), HeadPose.apply(s2w, floatArrayOf(0f, 0f, -1f)))
assertVec(floatArrayOf(0f, 0f, 1f), HeadPose.apply(s2w, floatArrayOf(0f, 1f, 0f)))
assertVec(HeadPose.IDENTITY, HeadPose.relative(HeadPose.recenter(s2w), s2w))
}
@Test fun glassesTurnedLeftLookLeft() {
val ref = HeadPose.recenter(HeadPose.fromGlasses(0f, 0f, 0f, 1f))
val q = quat(0f, 0f, 1f, 30.0)
val view = HeadPose.relative(ref, HeadPose.fromGlasses(q[0], q[1], q[2], q[3]))
val s = sin(PI / 6).toFloat(); val c = cos(PI / 6).toFloat()
assertVec(floatArrayOf(-s, 0f, -c), HeadPose.apply(view, floatArrayOf(0f, 0f, -1f)))
assertVec(floatArrayOf(0f, 1f, 0f), HeadPose.apply(view, floatArrayOf(0f, 1f, 0f)))
}
@Test fun glassesRecenterKeepsTheHorizon() {
val q = quat(0f, 0f, 1f, 75.0)
val ref = HeadPose.recenter(HeadPose.fromGlasses(q[0], q[1], q[2], q[3]))
assertVec(HeadPose.IDENTITY, HeadPose.relative(ref, HeadPose.fromGlasses(q[0], q[1], q[2], q[3])))
val tilt = quat(0f, 1f, 0f, 20.0)
val both = HeadPose.multiply(HeadPose.fromQuaternion(q[0], q[1], q[2], q[3]), HeadPose.fromQuaternion(tilt[0], tilt[1], tilt[2], tilt[3]))
val view = HeadPose.multiply(HeadPose.transpose(ref.basis), HeadPose.multiply(both, floatArrayOf(0f, 0f, -1f, -1f, 0f, 0f, 0f, 1f, 0f)))
val ahead = HeadPose.apply(view, floatArrayOf(0f, 0f, -1f))
assertEquals(0f, ahead[0], eps)
assertEquals(-sin(PI / 9).toFloat(), ahead[1], eps)
}
@Test fun monoIsNotPartOfTheSavedSettings() {
val c = Cardboard(enabled = true, mono = true)
assertEquals(false, Cardboard.decode(c.encode()).mono)
}
}
+6
View File
@@ -41,6 +41,12 @@ int main(void) {
q.source = CARDBOARD_SOURCE_SBS;
cardboard_push_constants(&q, pose, 3840, 1080, 2400, 1080, pc);
CHECK(near(pc[17], 1920.0f / 1080.0f) && near(pc[18], 1.0f));
q.mono = 1;
cardboard_push_constants(&q, pose, 3840, 1080, 1920, 1080, pc);
CHECK(near(pc[3], 0.0f) && near(pc[19], 1920.0f / 1080.0f) && near(pc[20], 1.0f));
q.mono = 0;
cardboard_push_constants(&q, pose, 3840, 1080, 1920, 1080, pc);
CHECK(near(pc[3], 0.032f) && near(pc[19], 960.0f / 1080.0f) && near(pc[20], 0.0f));
/* Unknown sizes fall back instead of dividing by zero. */
cardboard_push_constants(&q, pose, 0, 0, 0, 0, pc);
CHECK(isfinite(pc[17]) && isfinite(pc[19]));
+33
View File
@@ -485,6 +485,20 @@ def execute(args: argparse.Namespace, adb: str, serial: str) -> dict:
if command == "access":
return add_serial(checked(provider_call(adb, serial, "access"), "ACCESS_FAILED"), serial)
if command == "glasses":
request = {"op": args.glasses_op or "status"}
if args.glasses_op == "set":
request.update(key=args.key, value=args.value)
elif args.glasses_op == "simulate":
request["seconds"] = args.seconds
elif args.glasses_op == "mode":
request["mode"] = args.mode
elif args.glasses_op == "imu":
request["ms"] = args.ms
elif args.glasses_op == "raw":
request.update(msgId=args.msg_id, payload=args.payload or "")
return add_serial(checked(provider_call(adb, serial, "glasses", request=request), "GLASSES_FAILED"), serial)
if command == "logs":
state = get_state(adb, serial)
session = state.get("session", {})
@@ -652,6 +666,25 @@ def build_parser() -> argparse.ArgumentParser:
commands.add_parser("access", help="whether debugging tools are enabled on the device")
glasses = commands.add_parser("glasses", help="VITURE glasses: status, settings, head tracking test")
glasses_ops = glasses.add_subparsers(dest="glasses_op")
glasses_ops.add_parser("status")
glasses_ops.add_parser("allow", help="ask for USB control of the glasses again")
glasses_set = glasses_ops.add_parser("set", help="refreshHz, sbsHz, brightness, volume, sunblock, 3d, gameOnGlasses, headTracking, auto3d")
glasses_set.add_argument("key")
glasses_set.add_argument("value")
glasses_sim = glasses_ops.add_parser("simulate", help="feed a moving head pose (yaw, pitch and roll sweeps)")
glasses_sim.add_argument("--seconds", type=int, default=20)
glasses_ops.add_parser("stop-simulate")
glasses_ops.add_parser("query", help="ask the glasses for their display mode, brightness, volume and IMU state")
glasses_mode = glasses_ops.add_parser("mode", help="send a display mode byte, e.g. 0x31 (1080p60), 0x34 (1080p120), 0x32 (3D)")
glasses_mode.add_argument("mode")
glasses_imu = glasses_ops.add_parser("imu", help="stream the glasses' IMU for a moment and show what it reports")
glasses_imu.add_argument("--ms", type=int, default=2000)
glasses_raw = glasses_ops.add_parser("raw", help="send one MCU command (debugging tools)")
glasses_raw.add_argument("msg_id", help="hex message id, e.g. 0x0007")
glasses_raw.add_argument("payload", nargs="?", help="hex bytes")
logs = commands.add_parser("logs", help="pull the latest session artifact directory")
logs.add_argument("which", choices=["latest"])
logs.add_argument("destination", nargs="?")
+44 -9
View File
@@ -101,6 +101,8 @@ HmdQuaternion_t QuatFromMatrix(const float m[9]) {
return q;
}
double VsyncPeriodSeconds();
// IVRVirtualDisplay: the compositor renders the final frame into its own backbuffer and hands it
// here instead of scanning it out. What the handle is on Linux is what this logs (experiment).
class VirtualDisplay : public IVRVirtualDisplay {
@@ -115,10 +117,10 @@ public:
}
void WaitForPresent() override {}
bool GetTimeSinceLastVsync(float *seconds, uint64_t *counter) override {
// A free-running 60 Hz vsync.
// A free-running vsync at the panel's rate.
using namespace std::chrono;
double t = duration<double>(steady_clock::now().time_since_epoch()).count();
double period = 1.0 / 60.0;
double period = VsyncPeriodSeconds();
*seconds = (float)fmod(t, period);
*counter = (uint64_t)(t / period);
return true;
@@ -130,16 +132,20 @@ private:
};
// The vsync grid SteamVR paces against in driver-direct mode (Prop_DriverDirectModeSendsVsyncEvents):
// a free-running 60 Hz tick, declared a little ahead of time (ALVR's announcer, alvr/server_openvr/src/vsync.rs).
// a free-running tick at the panel's rate, declared a little ahead of time (ALVR's announcer, alvr/server_openvr/src/vsync.rs).
class Vsync {
public:
static constexpr std::chrono::nanoseconds kInterval{16666667};
static constexpr std::chrono::microseconds kLead{2000};
static std::chrono::nanoseconds Interval() { return std::chrono::nanoseconds(intervalNs_.load()); }
static void SetRate(float hz) {
if (hz >= 24.0f && hz <= 240.0f) intervalNs_ = (int64_t)(1e9 / hz);
}
static void Start() {
if (running_.exchange(true)) return;
thread_ = std::thread([] {
auto next = std::chrono::steady_clock::now() + kInterval;
auto next = std::chrono::steady_clock::now() + Interval();
while (running_) {
auto announce = next - kLead;
std::this_thread::sleep_until(announce);
@@ -150,9 +156,10 @@ public:
std::lock_guard<std::mutex> lock(mutex_);
last_ = next;
}
next += kInterval;
const auto interval = Interval();
next += interval;
now = std::chrono::steady_clock::now();
while (next <= now) next += kInterval;
while (next <= now) next += interval;
}
});
}
@@ -167,11 +174,14 @@ public:
private:
static inline std::atomic<bool> running_{false};
static inline std::atomic<int64_t> intervalNs_{16666667};
static inline std::thread thread_;
static inline std::mutex mutex_;
static inline std::chrono::steady_clock::time_point last_{};
};
double VsyncPeriodSeconds() { return Vsync::Interval().count() / 1e9; }
// IVRDriverDirectModeComponent: SteamVR renders each eye into textures this driver allocates
// (shared Vulkan images from SteamVR's IPC resource manager, dmabufs on Linux) and hands them
// over per frame. This is how ALVR gets frames on Linux; it avoids the compositor's own direct mode,
@@ -276,7 +286,7 @@ public:
void PostPresent(const Throttling_t *t) override {
// Hold until the tick after the last one declared (ALVR's pacing): a slow frame costs one frame.
auto last = Vsync::LastTick();
auto until = last + Vsync::kInterval * (1 + (t ? t->nFramesToThrottle : 0));
auto until = last + Vsync::Interval() * (1 + (t ? t->nFramesToThrottle : 0));
if (until > std::chrono::steady_clock::now()) std::this_thread::sleep_until(until);
}
@@ -468,6 +478,10 @@ public:
tanHalf_ = hp.tan_half_fov;
if (hp.eye_aspect > 0.0f) eyeAspect_ = hp.eye_aspect;
}
if (pose_.Read(&hp) && hp.display_hz >= 24.0f) hz = hp.display_hz;
Vsync::SetRate(hz);
sentTan_ = tanHalf_;
sentAspect_ = eyeAspect_;
VRProperties()->SetStringProperty(c, Prop_ModelNumber_String, "DroidDeck Cardboard");
VRProperties()->SetStringProperty(c, Prop_ManufacturerName_String, "DroidDeck");
VRProperties()->SetStringProperty(c, Prop_TrackingSystemName_String, "droiddeck");
@@ -551,7 +565,7 @@ public:
bool IsDisplayRealDisplay() override { return false; }
void GetRecommendedRenderTargetSize(uint32_t *w, uint32_t *h) override {
// Rendered a little larger than the panel: the lens pass magnifies the middle.
*w = (uint32_t)(width_ / 2 * 1.4f);
*w = (uint32_t)(height_ * 1.4f * eyeAspect_);
*h = (uint32_t)(height_ * 1.4f);
}
void GetEyeOutputViewport(EVREye eye, uint32_t *x, uint32_t *y, uint32_t *w, uint32_t *h) override {
@@ -575,6 +589,22 @@ public:
}
private:
void UpdateEye(const droiddeck_headpose &hp) {
sentTan_ = tanHalf_ = hp.tan_half_fov;
sentAspect_ = eyeAspect_ = hp.eye_aspect;
HmdRect2_t r;
r.vTopLeft.v[0] = -tanHalf_ * eyeAspect_;
r.vTopLeft.v[1] = -tanHalf_;
r.vBottomRight.v[0] = tanHalf_ * eyeAspect_;
r.vBottomRight.v[1] = tanHalf_;
VRServerDriverHost()->SetDisplayProjectionRaw(id_, r, r);
uint32_t w = 0, h = 0;
GetRecommendedRenderTargetSize(&w, &h);
VRServerDriverHost()->SetRecommendedRenderTargetSize(id_, w, h);
Log("eye now tan(half fov) %.3f, aspect %.3f (%ux%u eye%s), render %ux%u", tanHalf_, eyeAspect_, hp.eye_w, hp.eye_h,
(hp.flags & DROIDDECK_HEADPOSE_MONO) ? ", one eye shown" : "", w, h);
}
void PoseLoop() {
uint32_t lastSeq = 0;
auto lastSent = std::chrono::steady_clock::now();
@@ -589,6 +619,10 @@ private:
if ((fresh || padChanged || now - lastSent > std::chrono::milliseconds(100)) && id_ != k_unTrackedDeviceIndexInvalid) {
if (fresh) {
lastSeq = hp.seq;
if (hp.eye_w > 0 && hp.tan_half_fov > 0.0f && hp.eye_aspect > 0.0f &&
(fabsf(hp.tan_half_fov - sentTan_) > 1e-3f || fabsf(hp.eye_aspect - sentAspect_) > 1e-3f))
UpdateEye(hp);
if (hp.display_hz >= 24.0f) Vsync::SetRate(hp.display_hz);
const bool worn = (hp.flags & DROIDDECK_HEADPOSE_ENABLED) != 0;
if (worn != worn_) {
worn_ = worn;
@@ -609,6 +643,7 @@ private:
uint32_t id_ = k_unTrackedDeviceIndexInvalid;
int32_t width_ = 1920, height_ = 1080;
float tanHalf_ = 1.0f, eyeAspect_ = 0.889f;
float sentTan_ = 0.0f, sentAspect_ = 0.0f;
public:
void SetHands(CardboardController *left, CardboardController *right) { hands_[0] = left; hands_[1] = right; }