mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-13 03:51:10 +00:00
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:
1 parent
31d12a9f6c
commit
1e66ff17c1
37 files changed
+2624
-318
No files matched your search
@@ -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. */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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]));
|
||||
|
||||
@@ -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="?")
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
Reference in new issue
Block a user