Add VR locomotion: move/turn with thumbsticks while the menu is open
build / build (push) Successful in 2m25s
build / build (push) Successful in 2m25s
- Left thumbstick snap-turns, right thumbstick smooth-moves, letting the
player line themselves up with the fixed game quad ("virtual screen")
without needing physical room-scale space to do it in. Implemented via
a second OpenXR reference space (a "play space") derived from the real
LOCAL space by the inverse of the player's transform, so every existing
quad/hit-testing/rendering call site just needed g_local_space swapped
for it - no manual coordinate-transform math required elsewhere
- Generalized the ray/quad hit-testing in xr_input.c to a proper
oriented-plane intersection (needed once quads can rotate); verified it
degenerates to the old axis-aligned formula when unrotated, so the
keyboard and non-recentered menu are unaffected
- The menu launcher now re-centers in front of the player, correctly
oriented to face them, each time it's freshly reopened
- "Reset Position" (Main Menu tab) recenters using the player's current
real head pose - not a plain reset-to-origin, which only undid
thumbstick movement and ignored physical drift since the session
started. The same recenter runs once automatically at startup too, as a
fallback for when there's no complete saved position to use instead
- Position/orientation persists in questshock.config.json's new
player_position group (independent of savegames), including new
numeric-value support in the config file's hand-rolled JSON parser
(previously string-enums only)
This commit is contained in:
@@ -51,6 +51,11 @@ startup, in addition to (and independent of) the engine's own `prefs.txt`.
|
|||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"language": "english"
|
"language": "english"
|
||||||
|
},
|
||||||
|
"player_position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"z": 0.0,
|
||||||
|
"yaw_degrees": 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -94,3 +99,42 @@ project's own menu/keyboard chrome text (title bar, buttons, etc.).
|
|||||||
open. See README.md section 3 ("Game assets") for where `mfdfrn.res`/
|
open. See README.md section 3 ("Game assets") for where `mfdfrn.res`/
|
||||||
`mfdger.res` come from - the GOG Enhanced Edition installer alone doesn't
|
`mfdger.res` come from - the GOG Enhanced Edition installer alone doesn't
|
||||||
include them.
|
include them.
|
||||||
|
|
||||||
|
## Player Position (`player_position`)
|
||||||
|
|
||||||
|
Where the player is virtually standing/facing, independent of - and not
|
||||||
|
reset by - starting a new game or loading a save (same independence from
|
||||||
|
`prefs.txt`/savegames as every other group here). Set by moving around
|
||||||
|
in-headset (menu open, left thumbstick snap-turns, right thumbstick smooth-
|
||||||
|
moves) to line yourself up with the game quad ("virtual screen"). Height is
|
||||||
|
never included here - it always comes from your real tracked headset
|
||||||
|
height, not this file.
|
||||||
|
|
||||||
|
**Startup fallback**: only applied at startup if all three fields (`x`,
|
||||||
|
`z`, `yaw_degrees`) are present - a config file with none of them (or none
|
||||||
|
at all), or a partial/hand-edited one missing any of the three, falls back
|
||||||
|
to an automatic recenter instead (see **Reset Position** below), using
|
||||||
|
your real head pose at that moment. A complete saved position is applied
|
||||||
|
as-is, with no such recenter.
|
||||||
|
|
||||||
|
**Reset Position** (Main Menu tab) is a separate, live-only action, not a
|
||||||
|
config value: it recenters using your *current* real head pose, so the
|
||||||
|
game quad ends up directly ahead of wherever you're actually standing/
|
||||||
|
facing right now - not a plain "go back to `(0, 0, 0)`" (which would only
|
||||||
|
undo thumbstick movement, not any physical drift/rotation since the
|
||||||
|
session started). It's unrelated to `questshock.config.json` - it never
|
||||||
|
reads or writes this file.
|
||||||
|
|
||||||
|
### `x`, `z`
|
||||||
|
|
||||||
|
Horizontal position, in meters, on the play space's floor plane.
|
||||||
|
|
||||||
|
- **Type**: number
|
||||||
|
- **Restart required?**: No (live-appliable)
|
||||||
|
|
||||||
|
### `yaw_degrees`
|
||||||
|
|
||||||
|
Which way the player is virtually facing, in degrees.
|
||||||
|
|
||||||
|
- **Type**: number
|
||||||
|
- **Restart required?**: No (live-appliable)
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
|
|
||||||
#include "qs_config.h"
|
#include "qs_config.h"
|
||||||
|
|
||||||
|
#include "xr_input.h"
|
||||||
|
#include "xr_session.h"
|
||||||
|
|
||||||
#include "Prefs.h"
|
#include "Prefs.h"
|
||||||
#include "fullscrn.h"
|
#include "fullscrn.h"
|
||||||
#include "lg.h"
|
#include "lg.h"
|
||||||
@@ -143,6 +146,38 @@ static bool qs_json_skip_number(QsJsonParser *p) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parses a JSON number at the cursor into *out - unlike qs_json_skip_number()
|
||||||
|
// above (which every other option so far has only ever needed, since every
|
||||||
|
// existing option is a string enum), this one actually extracts the value.
|
||||||
|
// Same scan-then-copy-then-parse approach as qs_json_parse_string(), needed
|
||||||
|
// because the source buffer isn't NUL-terminated at an arbitrary offset, so
|
||||||
|
// strtof() can't just be pointed at the cursor directly.
|
||||||
|
static bool qs_json_parse_number(QsJsonParser *p, float *out) {
|
||||||
|
if (p->error)
|
||||||
|
return false;
|
||||||
|
const char *start = p->p;
|
||||||
|
if (p->p < p->end && *p->p == '-')
|
||||||
|
p->p++;
|
||||||
|
while (p->p < p->end && (isdigit((unsigned char)*p->p) || *p->p == '.' || *p->p == 'e' ||
|
||||||
|
*p->p == 'E' || *p->p == '+' || *p->p == '-'))
|
||||||
|
p->p++;
|
||||||
|
size_t len = (size_t)(p->p - start);
|
||||||
|
if (len == 0 || len >= 64) {
|
||||||
|
qs_json_fail(p);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char buf[64];
|
||||||
|
memcpy(buf, start, len);
|
||||||
|
buf[len] = '\0';
|
||||||
|
char *endptr = NULL;
|
||||||
|
*out = strtof(buf, &endptr);
|
||||||
|
if (endptr == buf) {
|
||||||
|
qs_json_fail(p);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool qs_json_skip_literal(QsJsonParser *p, const char *literal) {
|
static bool qs_json_skip_literal(QsJsonParser *p, const char *literal) {
|
||||||
size_t len = strlen(literal);
|
size_t len = strlen(literal);
|
||||||
if ((size_t)(p->end - p->p) < len || strncmp(p->p, literal, len) != 0) {
|
if ((size_t)(p->end - p->p) < len || strncmp(p->p, literal, len) != 0) {
|
||||||
@@ -275,6 +310,19 @@ static void qs_json_take_string_field(QsJsonParser *p, char *out, size_t out_siz
|
|||||||
qs_json_skip_value(p);
|
qs_json_skip_value(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same "skip if wrong type" behavior as qs_json_take_string_field() above,
|
||||||
|
// for a numeric field - sets *has to true only on success, since (unlike
|
||||||
|
// the string groups, where an empty string means "not present") 0.0f is a
|
||||||
|
// perfectly valid value and can't double as an absent-sentinel.
|
||||||
|
static void qs_json_take_number_field(QsJsonParser *p, float *out, bool *has) {
|
||||||
|
if (!p->error && p->p < p->end && (isdigit((unsigned char)*p->p) || *p->p == '-')) {
|
||||||
|
if (qs_json_parse_number(p, out))
|
||||||
|
*has = true;
|
||||||
|
} else {
|
||||||
|
qs_json_skip_value(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Schema: "video_options" group
|
// Schema: "video_options" group
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -288,11 +336,21 @@ typedef struct {
|
|||||||
char language[16]; // "" = not present in the file
|
char language[16]; // "" = not present in the file
|
||||||
} QsParsedGeneral;
|
} QsParsedGeneral;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float x, z, yaw_degrees;
|
||||||
|
// Unlike every string-valued option above, 0.0f is a perfectly valid
|
||||||
|
// value here and can't double as "not present in the file" - see
|
||||||
|
// qs_json_take_number_field()'s own comment.
|
||||||
|
bool has_x, has_z, has_yaw_degrees;
|
||||||
|
} QsParsedPlayerPosition;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
QsParsedVideoOptions video_options;
|
QsParsedVideoOptions video_options;
|
||||||
bool has_video_options;
|
bool has_video_options;
|
||||||
QsParsedGeneral general;
|
QsParsedGeneral general;
|
||||||
bool has_general;
|
bool has_general;
|
||||||
|
QsParsedPlayerPosition player_position;
|
||||||
|
bool has_player_position;
|
||||||
} QsParsedConfig;
|
} QsParsedConfig;
|
||||||
|
|
||||||
static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void *ctx) {
|
static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||||
@@ -313,12 +371,27 @@ static void qs_parse_general_field(const char *key, QsJsonParser *p, void *ctx)
|
|||||||
qs_json_skip_value(p);
|
qs_json_skip_value(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void qs_parse_player_position_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||||
|
QsParsedPlayerPosition *out = (QsParsedPlayerPosition *)ctx;
|
||||||
|
if (strcmp(key, "x") == 0)
|
||||||
|
qs_json_take_number_field(p, &out->x, &out->has_x);
|
||||||
|
else if (strcmp(key, "z") == 0)
|
||||||
|
qs_json_take_number_field(p, &out->z, &out->has_z);
|
||||||
|
else if (strcmp(key, "yaw_degrees") == 0)
|
||||||
|
qs_json_take_number_field(p, &out->yaw_degrees, &out->has_yaw_degrees);
|
||||||
|
else
|
||||||
|
qs_json_skip_value(p);
|
||||||
|
}
|
||||||
|
|
||||||
static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx) {
|
static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||||
QsParsedConfig *out = (QsParsedConfig *)ctx;
|
QsParsedConfig *out = (QsParsedConfig *)ctx;
|
||||||
if (strcmp(key, "video_options") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
if (strcmp(key, "video_options") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
||||||
out->has_video_options = qs_json_parse_object(p, qs_parse_video_options_field, &out->video_options);
|
out->has_video_options = qs_json_parse_object(p, qs_parse_video_options_field, &out->video_options);
|
||||||
else if (strcmp(key, "general") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
else if (strcmp(key, "general") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
||||||
out->has_general = qs_json_parse_object(p, qs_parse_general_field, &out->general);
|
out->has_general = qs_json_parse_object(p, qs_parse_general_field, &out->general);
|
||||||
|
else if (strcmp(key, "player_position") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
||||||
|
out->has_player_position =
|
||||||
|
qs_json_parse_object(p, qs_parse_player_position_field, &out->player_position);
|
||||||
else
|
else
|
||||||
qs_json_skip_value(p);
|
qs_json_skip_value(p);
|
||||||
}
|
}
|
||||||
@@ -569,6 +642,46 @@ static const char *qs_current_language_name(void) {
|
|||||||
return "english"; // shouldn't happen - safe fallback for Save
|
return "english"; // shouldn't happen - safe fallback for Save
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// General: player position
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// Live-appliable: xr_session_set_player_transform() (xr_session.c) is a
|
||||||
|
// plain state setter with no XR API calls of its own - safe to call before
|
||||||
|
// xr_init() has even run (which is how startup seeding works here, unlike
|
||||||
|
// every other option: nothing in xr_session.c exists yet to "apply" this
|
||||||
|
// to at startup, so the value is simply stored and picked up once
|
||||||
|
// xr_init() creates its own play space - see that function's own comment)
|
||||||
|
// as well as live, mid-session.
|
||||||
|
#define QS_PLAYER_POSITION_REQUIRES_RESTART false
|
||||||
|
|
||||||
|
// Applies only the fields actually present in *parsed, leaving the other
|
||||||
|
// axis/yaw untouched - e.g. a hand-edited file with just `"yaw_degrees"`
|
||||||
|
// shouldn't reset x/z to 0. Always live-appliable (see above) - the `live`
|
||||||
|
// parameter argument that qs_apply_renderer()/qs_apply_resolution()/
|
||||||
|
// qs_apply_language() take doesn't apply here, so unlike those this takes
|
||||||
|
// no such argument.
|
||||||
|
static bool qs_apply_player_position(const QsParsedPlayerPosition *parsed) {
|
||||||
|
float x, z, yaw_degrees;
|
||||||
|
xr_session_get_player_transform(&x, &z, &yaw_degrees);
|
||||||
|
bool changed = false;
|
||||||
|
if (parsed->has_x && parsed->x != x) {
|
||||||
|
x = parsed->x;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (parsed->has_z && parsed->z != z) {
|
||||||
|
z = parsed->z;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (parsed->has_yaw_degrees && parsed->yaw_degrees != yaw_degrees) {
|
||||||
|
yaw_degrees = parsed->yaw_degrees;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed)
|
||||||
|
xr_session_set_player_transform(x, z, yaw_degrees);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Read-only queries for the Video Options tab (see qs_config.h)
|
// Read-only queries for the Video Options tab (see qs_config.h)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -629,6 +742,11 @@ static void qs_config_apply_parsed(const QsParsedConfig *parsed, bool live, char
|
|||||||
qs_append_restart_reason(restart_needed, restart_needed_size, "language");
|
qs_append_restart_reason(restart_needed, restart_needed_size, "language");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (parsed->has_player_position) {
|
||||||
|
bool changed = qs_apply_player_position(&parsed->player_position);
|
||||||
|
if (changed && QS_PLAYER_POSITION_REQUIRES_RESTART)
|
||||||
|
qs_append_restart_reason(restart_needed, restart_needed_size, "player_position");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool qs_config_write_file(void) {
|
static bool qs_config_write_file(void) {
|
||||||
@@ -644,7 +762,16 @@ static bool qs_config_write_file(void) {
|
|||||||
fprintf(f, " },\n");
|
fprintf(f, " },\n");
|
||||||
fprintf(f, " \"general\": {\n");
|
fprintf(f, " \"general\": {\n");
|
||||||
fprintf(f, " \"language\": \"%s\"\n", qs_current_language_name());
|
fprintf(f, " \"language\": \"%s\"\n", qs_current_language_name());
|
||||||
|
fprintf(f, " },\n");
|
||||||
|
{
|
||||||
|
float x, z, yaw_degrees;
|
||||||
|
xr_session_get_player_transform(&x, &z, &yaw_degrees);
|
||||||
|
fprintf(f, " \"player_position\": {\n");
|
||||||
|
fprintf(f, " \"x\": %f,\n", x);
|
||||||
|
fprintf(f, " \"z\": %f,\n", z);
|
||||||
|
fprintf(f, " \"yaw_degrees\": %f\n", yaw_degrees);
|
||||||
fprintf(f, " }\n");
|
fprintf(f, " }\n");
|
||||||
|
}
|
||||||
fprintf(f, "}\n");
|
fprintf(f, "}\n");
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return true;
|
return true;
|
||||||
@@ -725,24 +852,38 @@ static Uint32 s_load_event_type;
|
|||||||
static Uint32 s_set_renderer_event_type;
|
static Uint32 s_set_renderer_event_type;
|
||||||
static Uint32 s_set_resolution_event_type;
|
static Uint32 s_set_resolution_event_type;
|
||||||
static Uint32 s_set_language_event_type;
|
static Uint32 s_set_language_event_type;
|
||||||
|
static Uint32 s_reset_position_event_type;
|
||||||
|
|
||||||
void qs_config_load_startup(void) {
|
void qs_config_load_startup(void) {
|
||||||
// Registers the custom SDL event types qs_config_request_save()/_load()/
|
// Registers the custom SDL event types qs_config_request_save()/_load()/
|
||||||
// _set_renderer()/_set_resolution()/_set_language() enqueue - done here,
|
// _set_renderer()/_set_resolution()/_set_language()/_reset_position()
|
||||||
// once, on the engine's own thread and before the menu/its buttons could
|
// enqueue - done here, once, on the engine's own thread and before the
|
||||||
// possibly exist yet, rather than lazily from those functions themselves
|
// menu/its buttons could possibly exist yet, rather than lazily from
|
||||||
// (which run on Android's UI thread and could otherwise race each other
|
// those functions themselves (which run on Android's UI thread and
|
||||||
// doing this lazily).
|
// could otherwise race each other doing this lazily).
|
||||||
Uint32 base = SDL_RegisterEvents(5);
|
Uint32 base = SDL_RegisterEvents(6);
|
||||||
s_save_event_type = base;
|
s_save_event_type = base;
|
||||||
s_load_event_type = base + 1;
|
s_load_event_type = base + 1;
|
||||||
s_set_renderer_event_type = base + 2;
|
s_set_renderer_event_type = base + 2;
|
||||||
s_set_resolution_event_type = base + 3;
|
s_set_resolution_event_type = base + 3;
|
||||||
s_set_language_event_type = base + 4;
|
s_set_language_event_type = base + 4;
|
||||||
|
s_reset_position_event_type = base + 5;
|
||||||
|
|
||||||
QsParsedConfig parsed;
|
QsParsedConfig parsed;
|
||||||
if (qs_config_read_file(&parsed))
|
if (qs_config_read_file(&parsed))
|
||||||
qs_config_apply_parsed(&parsed, /*live=*/false, NULL, 0);
|
qs_config_apply_parsed(&parsed, /*live=*/false, NULL, 0);
|
||||||
|
|
||||||
|
// A saved player_position only means anything if all three fields are
|
||||||
|
// present (a partial one - e.g. just "yaw_degrees" from a hand-edited
|
||||||
|
// file - isn't a complete, sane transform) - xr_input.c's own
|
||||||
|
// automatic startup recenter (xr_input_recenter_player_to_screen(),
|
||||||
|
// via g_pending_startup_recenter) is the fallback for a missing file,
|
||||||
|
// a missing player_position group, or an incomplete one; skip it here
|
||||||
|
// once a complete saved position was actually loaded, so it doesn't
|
||||||
|
// immediately override what was just applied above.
|
||||||
|
if (parsed.has_player_position && parsed.player_position.has_x &&
|
||||||
|
parsed.player_position.has_z && parsed.player_position.has_yaw_degrees)
|
||||||
|
xr_input_skip_startup_recenter();
|
||||||
}
|
}
|
||||||
|
|
||||||
void qs_config_request_save(void) {
|
void qs_config_request_save(void) {
|
||||||
@@ -783,6 +924,13 @@ void qs_config_request_set_language(int language_index) {
|
|||||||
SDL_PushEvent(&event);
|
SDL_PushEvent(&event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void qs_config_request_reset_position(void) {
|
||||||
|
SDL_Event event;
|
||||||
|
SDL_zero(event);
|
||||||
|
event.type = s_reset_position_event_type;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
}
|
||||||
|
|
||||||
bool qs_config_handle_event(const SDL_Event *event) {
|
bool qs_config_handle_event(const SDL_Event *event) {
|
||||||
if (event->type == s_save_event_type) {
|
if (event->type == s_save_event_type) {
|
||||||
if (qs_config_write_file())
|
if (qs_config_write_file())
|
||||||
@@ -821,5 +969,18 @@ bool qs_config_handle_event(const SDL_Event *event) {
|
|||||||
qs_apply_language(kLanguages[index].name, /*live=*/true);
|
qs_apply_language(kLanguages[index].name, /*live=*/true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (event->type == s_reset_position_event_type) {
|
||||||
|
// Deliberately not xr_session_set_player_transform(0,0,0) - see
|
||||||
|
// xr_input_recenter_player_to_screen()'s own comment: a "return to
|
||||||
|
// (0,0,0)" reset only undoes thumbstick locomotion, not any real,
|
||||||
|
// physical drift/rotation since the session started, which is what
|
||||||
|
// actually made this feel "slightly off" - recentering onto the
|
||||||
|
// player's current real head pose fixes both at once, and is
|
||||||
|
// unrelated to questshock.config.json (this event doesn't read or
|
||||||
|
// write the config file at all, just like this one never has).
|
||||||
|
xr_input_recenter_player_to_screen();
|
||||||
|
message_info("Recentered");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,13 +56,23 @@ void qs_config_request_set_resolution(int resolution_index);
|
|||||||
// menu applies a language change (see qs_config.c's qs_apply_language()).
|
// menu applies a language change (see qs_config.c's qs_apply_language()).
|
||||||
void qs_config_request_set_language(int language_index);
|
void qs_config_request_set_language(int language_index);
|
||||||
|
|
||||||
// Returns true and handles the event if it's one of the five custom events
|
// Same cross-thread-dispatch reasoning as the three above - called from the
|
||||||
|
// Main Menu tab's "Reset Position" button. A pure, live "return to a sane
|
||||||
|
// view of the game quad" action (see xr_input.h's own
|
||||||
|
// xr_input_recenter_player_to_screen(), which this dispatches to) -
|
||||||
|
// unrelated to questshock.config.json despite living in this file for the
|
||||||
|
// cross-thread dispatch machinery: it never reads or writes the config
|
||||||
|
// file, and isn't a "reset to (0,0,0)" - it recenters based on the
|
||||||
|
// player's current real head pose instead.
|
||||||
|
void qs_config_request_reset_position(void);
|
||||||
|
|
||||||
|
// Returns true and handles the event if it's one of the six custom events
|
||||||
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()/
|
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()/
|
||||||
// _set_language() enqueue - called from pump_events() (sdl_events.c, see
|
// _set_language()/_reset_position() enqueue - called from pump_events()
|
||||||
// android/engine-patches/21-android-config.patch) on the engine's own
|
// (sdl_events.c, see android/engine-patches/21-android-config.patch) on the
|
||||||
// thread, once per polled event, before its normal switch on well-known
|
// engine's own thread, once per polled event, before its normal switch on
|
||||||
// SDL_* event types. Returns false (no-op) for any other event type,
|
// well-known SDL_* event types. Returns false (no-op) for any other event
|
||||||
// including before the five custom types have even been registered.
|
// type, including before the six custom types have even been registered.
|
||||||
bool qs_config_handle_event(const SDL_Event *event);
|
bool qs_config_handle_event(const SDL_Event *event);
|
||||||
|
|
||||||
// Read-only queries for the Video Options tab's own selection highlight
|
// Read-only queries for the Video Options tab's own selection highlight
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetLanguage(JNIEnv *env, jcla
|
|||||||
qs_config_request_set_language(languageIndex);
|
qs_config_request_set_language(languageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MenuOverlay's Main Menu tab's "Reset Position" button - same enqueue-only
|
||||||
|
// reasoning as above.
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestResetPosition(JNIEnv *env, jclass clazz) {
|
||||||
|
qs_config_request_reset_position();
|
||||||
|
}
|
||||||
|
|
||||||
// Read-only queries - safe to call directly (no enqueue needed, see
|
// Read-only queries - safe to call directly (no enqueue needed, see
|
||||||
// qs_config.h's own comment on each of these).
|
// qs_config.h's own comment on each of these).
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
|
|||||||
@@ -20,6 +20,12 @@
|
|||||||
#define LEFT 0
|
#define LEFT 0
|
||||||
#define RIGHT 1
|
#define RIGHT 1
|
||||||
|
|
||||||
|
// Same constant (deliberately not <math.h>'s M_PI - see xr_overlay.c's own
|
||||||
|
// copy of this comment) as xr_session.c's play-space rebuild and
|
||||||
|
// xr_overlay.c's quad-rotation math - this file's ray/oriented-quad
|
||||||
|
// intersection and thumbstick-turn math need the same conversion.
|
||||||
|
#define QS_DEG_TO_RAD (3.14159265358979323846f / 180.0f)
|
||||||
|
|
||||||
static const char *kHandName[2] = {"left", "right"};
|
static const char *kHandName[2] = {"left", "right"};
|
||||||
static const float kHandColor[2][4] = {
|
static const float kHandColor[2][4] = {
|
||||||
{0.2f, 1.0f, 1.0f, 1.0f}, // left: cyan
|
{0.2f, 1.0f, 1.0f, 1.0f}, // left: cyan
|
||||||
@@ -63,6 +69,10 @@ static XrAction g_space_key_action = XR_NULL_HANDLE;
|
|||||||
// SDL_TEXTINPUT needed (see the space_key injection below for why that
|
// SDL_TEXTINPUT needed (see the space_key injection below for why that
|
||||||
// matters).
|
// matters).
|
||||||
static XrAction g_forward_key_action = XR_NULL_HANDLE;
|
static XrAction g_forward_key_action = XR_NULL_HANDLE;
|
||||||
|
// Player locomotion while the menu is open (see xr_session.h's
|
||||||
|
// xr_session_set_player_transform()) - left stick snap-turns, right stick
|
||||||
|
// smooth-moves, both read from the same Vector2f action via subactionPath.
|
||||||
|
static XrAction g_thumbstick_action = XR_NULL_HANDLE;
|
||||||
static XrPath g_hand_path[2];
|
static XrPath g_hand_path[2];
|
||||||
static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
|
static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
|
||||||
|
|
||||||
@@ -75,6 +85,25 @@ static bool g_prev_select[2] = {false, false};
|
|||||||
static bool g_prev_menu = false;
|
static bool g_prev_menu = false;
|
||||||
static bool g_prev_space_key = false;
|
static bool g_prev_space_key = false;
|
||||||
static bool g_prev_forward_key = false;
|
static bool g_prev_forward_key = false;
|
||||||
|
// Snap-turn hysteresis - true when the left stick has returned near center
|
||||||
|
// since the last turn it triggered, i.e. it's "armed" to trigger another
|
||||||
|
// one; false from the moment a turn triggers until the stick re-centers,
|
||||||
|
// so holding the stick deflected turns exactly once, not once per frame.
|
||||||
|
static bool g_turn_ready = true;
|
||||||
|
// Self-contained delta-time tracker for smooth movement - deliberately not
|
||||||
|
// shared with xr_session.c's own SDL_GetTicks()-based FPS smoothing, which
|
||||||
|
// is a private implementation detail of that file's stats display.
|
||||||
|
static Uint32 g_last_move_ticks = 0;
|
||||||
|
// True until either xr_input_recenter_player_to_screen() has succeeded
|
||||||
|
// once this session, or xr_input_skip_startup_recenter() has been called -
|
||||||
|
// checked once per frame in xr_input_sync_and_draw() (see its own comment
|
||||||
|
// there) as a *fallback* default player position/orientation for when
|
||||||
|
// there's no complete saved one to fall back on instead (see
|
||||||
|
// qs_config_load_startup()'s own comment on when it calls
|
||||||
|
// xr_input_skip_startup_recenter()). While still pending, retries every
|
||||||
|
// frame until a valid head pose is actually available - typically the
|
||||||
|
// very first frame, but tracking isn't guaranteed to be valid that early.
|
||||||
|
static bool g_pending_startup_recenter = true;
|
||||||
static bool g_game_prev_hit[2] = {false, false};
|
static bool g_game_prev_hit[2] = {false, false};
|
||||||
static bool g_game_touch_active[2] = {false, false};
|
static bool g_game_touch_active[2] = {false, false};
|
||||||
static bool g_menu_prev_hit[2] = {false, false};
|
static bool g_menu_prev_hit[2] = {false, false};
|
||||||
@@ -282,6 +311,17 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
|
|||||||
"xrCreateAction(forward_key)"))
|
"xrCreateAction(forward_key)"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Player locomotion, see g_thumbstick_action's own declaration comment.
|
||||||
|
XrActionCreateInfo thumbstickInfo = {XR_TYPE_ACTION_CREATE_INFO};
|
||||||
|
strncpy(thumbstickInfo.actionName, "thumbstick", XR_MAX_ACTION_NAME_SIZE - 1);
|
||||||
|
strncpy(thumbstickInfo.localizedActionName, "Thumbstick", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
||||||
|
thumbstickInfo.actionType = XR_ACTION_TYPE_VECTOR2F_INPUT;
|
||||||
|
thumbstickInfo.countSubactionPaths = 2;
|
||||||
|
thumbstickInfo.subactionPaths = g_hand_path;
|
||||||
|
if (!xr_check(xrCreateAction(g_action_set, &thumbstickInfo, &g_thumbstick_action),
|
||||||
|
"xrCreateAction(thumbstick)"))
|
||||||
|
return false;
|
||||||
|
|
||||||
XrPath profilePath;
|
XrPath profilePath;
|
||||||
xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &profilePath);
|
xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &profilePath);
|
||||||
|
|
||||||
@@ -294,7 +334,7 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
|
|||||||
// have at all (like a nonexistent .../trigger/click) fails the entire
|
// have at all (like a nonexistent .../trigger/click) fails the entire
|
||||||
// xrSuggestInteractionProfileBindings call, not just that one entry.
|
// xrSuggestInteractionProfileBindings call, not just that one entry.
|
||||||
XrPath aimPoseLeftPath, aimPoseRightPath, triggerLeftPath, triggerRightPath, menuClickPath,
|
XrPath aimPoseLeftPath, aimPoseRightPath, triggerLeftPath, triggerRightPath, menuClickPath,
|
||||||
aClickPath, bClickPath;
|
aClickPath, bClickPath, thumbstickLeftPath, thumbstickRightPath;
|
||||||
xrStringToPath(instance, "/user/hand/left/input/aim/pose", &aimPoseLeftPath);
|
xrStringToPath(instance, "/user/hand/left/input/aim/pose", &aimPoseLeftPath);
|
||||||
xrStringToPath(instance, "/user/hand/right/input/aim/pose", &aimPoseRightPath);
|
xrStringToPath(instance, "/user/hand/right/input/aim/pose", &aimPoseRightPath);
|
||||||
xrStringToPath(instance, "/user/hand/left/input/trigger/value", &triggerLeftPath);
|
xrStringToPath(instance, "/user/hand/left/input/trigger/value", &triggerLeftPath);
|
||||||
@@ -302,6 +342,8 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
|
|||||||
xrStringToPath(instance, "/user/hand/left/input/menu/click", &menuClickPath);
|
xrStringToPath(instance, "/user/hand/left/input/menu/click", &menuClickPath);
|
||||||
xrStringToPath(instance, "/user/hand/right/input/a/click", &aClickPath);
|
xrStringToPath(instance, "/user/hand/right/input/a/click", &aClickPath);
|
||||||
xrStringToPath(instance, "/user/hand/right/input/b/click", &bClickPath);
|
xrStringToPath(instance, "/user/hand/right/input/b/click", &bClickPath);
|
||||||
|
xrStringToPath(instance, "/user/hand/left/input/thumbstick", &thumbstickLeftPath);
|
||||||
|
xrStringToPath(instance, "/user/hand/right/input/thumbstick", &thumbstickRightPath);
|
||||||
|
|
||||||
XrActionSuggestedBinding bindings[] = {
|
XrActionSuggestedBinding bindings[] = {
|
||||||
{g_aim_pose_action, aimPoseLeftPath}, {g_aim_pose_action, aimPoseRightPath},
|
{g_aim_pose_action, aimPoseLeftPath}, {g_aim_pose_action, aimPoseRightPath},
|
||||||
@@ -309,6 +351,7 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
|
|||||||
{g_menu_toggle_action, menuClickPath},
|
{g_menu_toggle_action, menuClickPath},
|
||||||
{g_space_key_action, aClickPath},
|
{g_space_key_action, aClickPath},
|
||||||
{g_forward_key_action, bClickPath},
|
{g_forward_key_action, bClickPath},
|
||||||
|
{g_thumbstick_action, thumbstickLeftPath}, {g_thumbstick_action, thumbstickRightPath},
|
||||||
};
|
};
|
||||||
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
||||||
suggested.interactionProfile = profilePath;
|
suggested.interactionProfile = profilePath;
|
||||||
@@ -387,28 +430,51 @@ static bool xr_input_try_overlay(XrOverlay *overlay, float titleBarVFraction,
|
|||||||
bool hadTouch = *touchActive;
|
bool hadTouch = *touchActive;
|
||||||
bool hadDrag = dragState->dragging && dragState->hand == hand;
|
bool hadDrag = dragState->dragging && dragState->hand == hand;
|
||||||
|
|
||||||
float quadCenterX, quadCenterY, distance, halfWidth, halfHeight;
|
float quadCenterX, quadCenterY, distance, halfWidth, halfHeight, yawDeg;
|
||||||
xr_overlay_get_quad_extent(overlay, &quadCenterX, &quadCenterY, &distance, &halfWidth,
|
xr_overlay_get_quad_extent(overlay, &quadCenterX, &quadCenterY, &distance, &halfWidth,
|
||||||
&halfHeight);
|
&halfHeight, &yawDeg);
|
||||||
|
|
||||||
// The quad's plane is z = -distance (facing the viewer along -Z) - a
|
// General ray/oriented-quad intersection - the quad's plane passes
|
||||||
// ray parallel to it (fz ~ 0) never crosses. worldX/Y is the raw
|
// through (quadCenterX, quadCenterY, -distance) with normal
|
||||||
// world-space intersection point, needed as-is for dragging (which can
|
// R_y(-yawDeg)*(0,0,1) (see xr_overlay.c's own pose-building comment on
|
||||||
// legitimately move the quad to where the ray isn't currently hitting
|
// the same rotation). Solved by rotating the ray's origin (relative to
|
||||||
// it); cx/cy are that same point in the quad's own [-1,1] local space;
|
// the quad's center) and direction into the quad's own local frame via
|
||||||
// hit/u/v (the equivalent 0..1, top-left-origin coordinates the Java
|
// R_y(+yawDeg) - the inverse of the render-time rotation - and
|
||||||
// view's pixel grid and the on-quad hit logs use) are only meaningful
|
// intersecting against the local z=0 plane there; A/B below are that
|
||||||
// within the quad's current bounds, unlike planeHit.
|
// rotated frame's z-components of the (center-relative) origin and the
|
||||||
|
// direction, so `t = -A/B` solves local_z(t) = A + t*B = 0. Degenerates
|
||||||
|
// to exactly the old plain "z = -distance, no rotation" formula when
|
||||||
|
// yawDeg is 0 (cosPhi=1, sinPhi=0) - true for every overlay except the
|
||||||
|
// menu launcher right after xr_input_sync_and_draw() has re-centered it
|
||||||
|
// in front of a turned player (see its own comment there).
|
||||||
|
float phi = yawDeg * QS_DEG_TO_RAD;
|
||||||
|
float cosPhi = cosf(phi), sinPhi = sinf(phi);
|
||||||
|
float dx0 = location->pose.position.x - quadCenterX;
|
||||||
|
float dz0 = location->pose.position.z + distance;
|
||||||
|
float a = -dx0 * sinPhi + dz0 * cosPhi;
|
||||||
|
float b = -fx * sinPhi + fz * cosPhi;
|
||||||
|
|
||||||
|
// worldX/Y is the raw base-space intersection point, needed as-is for
|
||||||
|
// dragging (which can legitimately move the quad to where the ray
|
||||||
|
// isn't currently hitting it, and always operates in base-space X/Y via
|
||||||
|
// xr_overlay_set_position() regardless of any rotation); cx/cy are that
|
||||||
|
// same point in the quad's own [-1,1] local space; hit/u/v (the
|
||||||
|
// equivalent 0..1, top-left-origin coordinates the Java view's pixel
|
||||||
|
// grid and the on-quad hit logs use) are only meaningful within the
|
||||||
|
// quad's current bounds, unlike planeHit.
|
||||||
bool planeHit = false, hit = false;
|
bool planeHit = false, hit = false;
|
||||||
float u = 0.0f, v = 0.0f, worldX = 0.0f, worldY = 0.0f, t = 0.0f;
|
float u = 0.0f, v = 0.0f, worldX = 0.0f, worldY = 0.0f, t = 0.0f;
|
||||||
if (fabsf(fz) > 1e-5f) {
|
if (fabsf(b) > 1e-5f) {
|
||||||
t = (-distance - location->pose.position.z) / fz;
|
t = -a / b;
|
||||||
if (t > 0.0f) {
|
if (t > 0.0f) {
|
||||||
planeHit = true;
|
planeHit = true;
|
||||||
worldX = location->pose.position.x + t * fx;
|
worldX = location->pose.position.x + t * fx;
|
||||||
worldY = location->pose.position.y + t * fy;
|
worldY = location->pose.position.y + t * fy;
|
||||||
float cx = (worldX - quadCenterX) / halfWidth;
|
float worldZ = location->pose.position.z + t * fz;
|
||||||
float cy = (worldY - quadCenterY) / halfHeight;
|
float localX = (worldX - quadCenterX) * cosPhi + (worldZ + distance) * sinPhi;
|
||||||
|
float localY = worldY - quadCenterY;
|
||||||
|
float cx = localX / halfWidth;
|
||||||
|
float cy = localY / halfHeight;
|
||||||
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
|
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
|
||||||
u = (cx + 1.0f) * 0.5f;
|
u = (cx + 1.0f) * 0.5f;
|
||||||
v = (1.0f - cy) * 0.5f;
|
v = (1.0f - cy) * 0.5f;
|
||||||
@@ -619,6 +685,126 @@ static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, fl
|
|||||||
return hit;
|
return hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-centers the menu launcher in front of wherever the player is
|
||||||
|
// currently looking, each time it's freshly opened (see the menu-toggle
|
||||||
|
// handling below) - otherwise a player who's walked/turned away from the
|
||||||
|
// menu's last position (via the thumbstick locomotion further down, or a
|
||||||
|
// manual drag) would find it stuck somewhere behind/beside them. Uses
|
||||||
|
// xr_overlay_get_default_distance_m() (the overlay's original, never-
|
||||||
|
// mutated distance_m) rather than duplicating MENU_DISTANCE_METERS here -
|
||||||
|
// deliberately NOT the live distance_m from xr_overlay_get_quad_extent(),
|
||||||
|
// which xr_overlay_set_pose() repurposes as a raw base-space Z coordinate
|
||||||
|
// once rotation is involved (see that function's own comment) - reusing
|
||||||
|
// that as a "how far in front" magnitude would drift the menu further
|
||||||
|
// away with every reopen instead of holding a stable distance. Height
|
||||||
|
// (center_y_m) doesn't have this problem (it's never touched by any
|
||||||
|
// rotation math), so that one *is* read live, preserving a manual drag.
|
||||||
|
// Independent of needBeams/draw below (a menu toggle can happen on a frame
|
||||||
|
// nothing renders), so this does its own dedicated head locate rather than
|
||||||
|
// reusing headLoc.
|
||||||
|
static void xr_input_recenter_menu(XrOverlay *menuOverlay, XrSpace baseSpace, XrTime time) {
|
||||||
|
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
|
||||||
|
const XrSpaceLocationFlags needed =
|
||||||
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
||||||
|
if (!xr_check(xrLocateSpace(g_view_space, baseSpace, time, &headLoc),
|
||||||
|
"xrLocateSpace(view, menu recenter)") ||
|
||||||
|
(headLoc.locationFlags & needed) != needed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float fx, fy, fz;
|
||||||
|
quat_rotate_vec(&headLoc.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz);
|
||||||
|
// Horizontal-only, matching the play space's own yaw-only rotation -
|
||||||
|
// looking sharply up/down shouldn't tip the menu with you.
|
||||||
|
float len = sqrtf(fx * fx + fz * fz);
|
||||||
|
if (len < 1e-4f)
|
||||||
|
return; // looking almost straight up/down - leave the existing pose alone
|
||||||
|
fx /= len;
|
||||||
|
fz /= len;
|
||||||
|
|
||||||
|
float currentX, currentY, liveDistance, halfWidth, halfHeight, currentYaw;
|
||||||
|
xr_overlay_get_quad_extent(menuOverlay, ¤tX, ¤tY, &liveDistance, &halfWidth,
|
||||||
|
&halfHeight, ¤tYaw);
|
||||||
|
(void)currentX;
|
||||||
|
(void)liveDistance;
|
||||||
|
(void)halfWidth;
|
||||||
|
(void)halfHeight;
|
||||||
|
(void)currentYaw;
|
||||||
|
float distance = xr_overlay_get_default_distance_m(menuOverlay);
|
||||||
|
|
||||||
|
float newX = headLoc.pose.position.x + fx * distance;
|
||||||
|
float newZ = headLoc.pose.position.z + fz * distance;
|
||||||
|
// xr_overlay_set_pose()'s distance_m is simply the quad's base-space Z
|
||||||
|
// position negated (see xr_overlay_render_and_build_layer()) - not
|
||||||
|
// necessarily positive once the quad can end up anywhere the player
|
||||||
|
// has walked to, unlike every other overlay's fixed, always-in-front-
|
||||||
|
// of-the-origin spawn pose.
|
||||||
|
float newDistance = -newZ;
|
||||||
|
// See xr_overlay_render_and_build_layer()'s own rotation comment -
|
||||||
|
// solved so the quad's local front normal (0,0,1) ends up pointing
|
||||||
|
// along -forward, i.e. back at the player looking along +forward at it.
|
||||||
|
float newYawDeg = -atan2f(-fx, -fz) / QS_DEG_TO_RAD;
|
||||||
|
|
||||||
|
xr_overlay_set_pose(menuOverlay, newX, currentY, newDistance, newYawDeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See xr_input.h's own comment. Chooses a new player transform such that
|
||||||
|
// the player's CURRENT real head pose maps to play-space position (0,0)
|
||||||
|
// facing (0,0,-1) - i.e. exactly reproducing this app's own startup
|
||||||
|
// framing (the fixed game quad dead ahead at its authored distance), just
|
||||||
|
// re-anchored to wherever the player is standing/facing right now instead
|
||||||
|
// of wherever their headset happened to be when the session first began.
|
||||||
|
// Deliberately locates the head against the REAL local space (via
|
||||||
|
// xr_session_get_local_space()), not the play space every other consumer
|
||||||
|
// in this file uses - computing a brand new transform from the player's
|
||||||
|
// real pose would be circular if measured through the very transform
|
||||||
|
// being replaced.
|
||||||
|
//
|
||||||
|
// Math: with V(p) = R_y(yaw_deg) * p + (x, z) defined as "real-space pose p
|
||||||
|
// maps to this play-space pose" (see xr_session_rebuild_play_space()'s own
|
||||||
|
// derivation, which builds the inverse of this same V), solving
|
||||||
|
// V(headPos) = (0,0) and V(headForward) = (0,0,-1) for (x, z, yaw_deg)
|
||||||
|
// gives the formulas below - verified by substitution that a
|
||||||
|
// player standing at the real origin facing the real default -Z (i.e. one
|
||||||
|
// who has never moved) yields (0, 0, 0), the same as this feature's
|
||||||
|
// original naive reset-to-identity behavior.
|
||||||
|
bool xr_input_recenter_player_to_screen(void) {
|
||||||
|
XrSpace localSpace = xr_session_get_local_space();
|
||||||
|
if (localSpace == XR_NULL_HANDLE || g_view_space == XR_NULL_HANDLE)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
XrTime time = xr_session_get_predicted_display_time();
|
||||||
|
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
|
||||||
|
const XrSpaceLocationFlags needed =
|
||||||
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
||||||
|
if (!xr_check(xrLocateSpace(g_view_space, localSpace, time, &headLoc),
|
||||||
|
"xrLocateSpace(view, recenter to screen)") ||
|
||||||
|
(headLoc.locationFlags & needed) != needed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
float fx, fy, fz;
|
||||||
|
quat_rotate_vec(&headLoc.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz);
|
||||||
|
float len = sqrtf(fx * fx + fz * fz);
|
||||||
|
if (len < 1e-4f)
|
||||||
|
return false; // looking almost straight up/down - nothing sane to recenter onto
|
||||||
|
fx /= len;
|
||||||
|
fz /= len;
|
||||||
|
|
||||||
|
// angle(0,-1) - angle(fx,fz), using the same atan2f(x, z) "angle"
|
||||||
|
// convention as xr_overlay_render_and_build_layer()'s own rotation
|
||||||
|
// comment (not the more usual atan2f(y, x)).
|
||||||
|
float theta = atan2f(0.0f, -1.0f) - atan2f(fx, fz);
|
||||||
|
float cosTheta = cosf(theta), sinTheta = sinf(theta);
|
||||||
|
float hx = headLoc.pose.position.x, hz = headLoc.pose.position.z;
|
||||||
|
float newX = -(hx * cosTheta + hz * sinTheta);
|
||||||
|
float newZ = hx * sinTheta - hz * cosTheta;
|
||||||
|
float newYawDeg = theta / QS_DEG_TO_RAD;
|
||||||
|
|
||||||
|
xr_session_set_player_transform(newX, newZ, newYawDeg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void xr_input_skip_startup_recenter(void) { g_pending_startup_recenter = false; }
|
||||||
|
|
||||||
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||||
if (g_action_set == XR_NULL_HANDLE)
|
if (g_action_set == XR_NULL_HANDLE)
|
||||||
return;
|
return;
|
||||||
@@ -629,6 +815,12 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
|||||||
syncInfo.activeActionSets = &activeSet;
|
syncInfo.activeActionSets = &activeSet;
|
||||||
xr_check(xrSyncActions(g_session, &syncInfo), "xrSyncActions");
|
xr_check(xrSyncActions(g_session, &syncInfo), "xrSyncActions");
|
||||||
|
|
||||||
|
// See g_pending_startup_recenter's own comment - retried every frame
|
||||||
|
// (cheap: one xrLocateSpace call) until head tracking actually has a
|
||||||
|
// valid pose to recenter onto, typically the very first frame.
|
||||||
|
if (g_pending_startup_recenter && xr_input_recenter_player_to_screen())
|
||||||
|
g_pending_startup_recenter = false;
|
||||||
|
|
||||||
XrOverlay *menuOverlay = xr_session_get_menu_overlay();
|
XrOverlay *menuOverlay = xr_session_get_menu_overlay();
|
||||||
XrOverlay *keyboardOverlay = xr_session_get_keyboard_overlay();
|
XrOverlay *keyboardOverlay = xr_session_get_keyboard_overlay();
|
||||||
bool menuVisible = xr_overlay_is_visible(menuOverlay);
|
bool menuVisible = xr_overlay_is_visible(menuOverlay);
|
||||||
@@ -702,14 +894,49 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
|||||||
// fully independent (its own Close button is the only way
|
// fully independent (its own Close button is the only way
|
||||||
// to hide it, see KeyboardOverlay.java).
|
// to hide it, see KeyboardOverlay.java).
|
||||||
if (menuOverlay != NULL) {
|
if (menuOverlay != NULL) {
|
||||||
|
bool wasHidden = !menuVisible;
|
||||||
xr_overlay_toggle_visible(menuOverlay);
|
xr_overlay_toggle_visible(menuOverlay);
|
||||||
g_menu_prev_hit[LEFT] = g_menu_prev_hit[RIGHT] = false;
|
g_menu_prev_hit[LEFT] = g_menu_prev_hit[RIGHT] = false;
|
||||||
g_menu_touch_active[LEFT] = g_menu_touch_active[RIGHT] = false;
|
g_menu_touch_active[LEFT] = g_menu_touch_active[RIGHT] = false;
|
||||||
|
if (wasHidden && xr_overlay_is_visible(menuOverlay))
|
||||||
|
xr_input_recenter_menu(menuOverlay, baseSpace, time);
|
||||||
}
|
}
|
||||||
} else if (!menuDown && g_prev_menu) {
|
} else if (!menuDown && g_prev_menu) {
|
||||||
LOGI("XR: menu-toggle button UP");
|
LOGI("XR: menu-toggle button UP");
|
||||||
}
|
}
|
||||||
g_prev_menu = menuDown;
|
g_prev_menu = menuDown;
|
||||||
|
|
||||||
|
// Snap turn - only while the menu is open (per the feature's
|
||||||
|
// own design: repositioning is for lining yourself up with the
|
||||||
|
// game quad from the menu, not a general gameplay locomotion
|
||||||
|
// scheme). g_turn_ready is a hysteresis latch (see its own
|
||||||
|
// declaration comment) so holding the stick past the threshold
|
||||||
|
// turns exactly once, not once per frame - it only re-arms once
|
||||||
|
// the stick has come back near center.
|
||||||
|
if (menuVisible) {
|
||||||
|
XrActionStateGetInfo turnInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||||
|
turnInfo.action = g_thumbstick_action;
|
||||||
|
turnInfo.subactionPath = g_hand_path[LEFT];
|
||||||
|
XrActionStateVector2f turnState = {XR_TYPE_ACTION_STATE_VECTOR2F};
|
||||||
|
xrGetActionStateVector2f(g_session, &turnInfo, &turnState);
|
||||||
|
if (turnState.isActive) {
|
||||||
|
const float kSnapThreshold = 0.7f;
|
||||||
|
const float kSnapRearm = 0.3f;
|
||||||
|
const float kSnapDegrees = 30.0f;
|
||||||
|
float x = turnState.currentState.x;
|
||||||
|
if (g_turn_ready && fabsf(x) > kSnapThreshold) {
|
||||||
|
float px, pz, pyaw;
|
||||||
|
xr_session_get_player_transform(&px, &pz, &pyaw);
|
||||||
|
// Confirmed backwards on-device from the initial
|
||||||
|
// (unverifiable-without-a-headset) guess - flipped.
|
||||||
|
pyaw += (x > 0.0f) ? -kSnapDegrees : kSnapDegrees;
|
||||||
|
xr_session_set_player_transform(px, pz, pyaw);
|
||||||
|
g_turn_ready = false;
|
||||||
|
} else if (fabsf(x) < kSnapRearm) {
|
||||||
|
g_turn_ready = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hand == RIGHT) {
|
if (hand == RIGHT) {
|
||||||
@@ -786,6 +1013,63 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
|||||||
SDL_PushEvent(&event);
|
SDL_PushEvent(&event);
|
||||||
g_prev_forward_key = forwardKeyDown;
|
g_prev_forward_key = forwardKeyDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Smooth move - only while the menu is open, same reasoning as
|
||||||
|
// the left stick's snap turn above. g_last_move_ticks is
|
||||||
|
// refreshed every frame regardless of menuVisible (not just
|
||||||
|
// while actively moving) so dt is always "time since the
|
||||||
|
// previous frame" and never goes stale/huge across a stretch
|
||||||
|
// with the menu closed.
|
||||||
|
Uint32 nowTicks = SDL_GetTicks();
|
||||||
|
Uint32 lastMoveTicks = g_last_move_ticks;
|
||||||
|
g_last_move_ticks = nowTicks;
|
||||||
|
if (menuVisible && lastMoveTicks != 0) {
|
||||||
|
XrActionStateGetInfo moveInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||||
|
moveInfo.action = g_thumbstick_action;
|
||||||
|
moveInfo.subactionPath = g_hand_path[RIGHT];
|
||||||
|
XrActionStateVector2f moveState = {XR_TYPE_ACTION_STATE_VECTOR2F};
|
||||||
|
xrGetActionStateVector2f(g_session, &moveInfo, &moveState);
|
||||||
|
const float kMoveDeadzone = 0.15f;
|
||||||
|
float mx = moveState.currentState.x, my = moveState.currentState.y;
|
||||||
|
float mag = sqrtf(mx * mx + my * my);
|
||||||
|
if (moveState.isActive && mag > kMoveDeadzone) {
|
||||||
|
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
|
||||||
|
const XrSpaceLocationFlags neededHead2 =
|
||||||
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
||||||
|
if (xr_check(xrLocateSpace(g_view_space, baseSpace, time, &headLoc),
|
||||||
|
"xrLocateSpace(view, move)") &&
|
||||||
|
(headLoc.locationFlags & neededHead2) == neededHead2) {
|
||||||
|
float hfx, hfy, hfz;
|
||||||
|
quat_rotate_vec(&headLoc.pose.orientation, 0.0f, 0.0f, -1.0f, &hfx, &hfy,
|
||||||
|
&hfz);
|
||||||
|
float horizLen = sqrtf(hfx * hfx + hfz * hfz);
|
||||||
|
if (horizLen > 1e-4f) {
|
||||||
|
hfx /= horizLen;
|
||||||
|
hfz /= horizLen;
|
||||||
|
// "Right" relative to forward, in the horizontal
|
||||||
|
// plane - R_y(-90deg) applied to (hfx, hfz), see
|
||||||
|
// xr_overlay.c's own rotation-matrix comment for
|
||||||
|
// the same R_y() convention.
|
||||||
|
float rfx = -hfz, rfz = hfx;
|
||||||
|
const float kMoveSpeedMetersPerSec = 1.2f;
|
||||||
|
float dt = (float)(nowTicks - lastMoveTicks) / 1000.0f;
|
||||||
|
float moveX = (hfx * my + rfx * mx) * kMoveSpeedMetersPerSec * dt;
|
||||||
|
float moveZ = (hfz * my + rfz * mx) * kMoveSpeedMetersPerSec * dt;
|
||||||
|
float px, pz, pyaw;
|
||||||
|
xr_session_get_player_transform(&px, &pz, &pyaw);
|
||||||
|
// Moves the play space opposite the player's
|
||||||
|
// intended travel, so quads (fixed in play
|
||||||
|
// space) get closer/farther as the player
|
||||||
|
// virtually walks toward/away from them - see
|
||||||
|
// xr_session_set_player_transform()'s own
|
||||||
|
// comment. Confirmed backwards on-device from
|
||||||
|
// the initial (unverifiable-without-a-headset)
|
||||||
|
// guess - flipped.
|
||||||
|
xr_session_set_player_transform(px + moveX, pz + moveZ, pyaw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
XrActionStateGetInfo poseInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
XrActionStateGetInfo poseInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||||
@@ -919,6 +1203,7 @@ void xr_input_shutdown(void) {
|
|||||||
g_menu_toggle_action = XR_NULL_HANDLE;
|
g_menu_toggle_action = XR_NULL_HANDLE;
|
||||||
g_space_key_action = XR_NULL_HANDLE;
|
g_space_key_action = XR_NULL_HANDLE;
|
||||||
g_forward_key_action = XR_NULL_HANDLE;
|
g_forward_key_action = XR_NULL_HANDLE;
|
||||||
|
g_thumbstick_action = XR_NULL_HANDLE;
|
||||||
|
|
||||||
g_instance = XR_NULL_HANDLE;
|
g_instance = XR_NULL_HANDLE;
|
||||||
g_session = XR_NULL_HANDLE;
|
g_session = XR_NULL_HANDLE;
|
||||||
@@ -937,4 +1222,7 @@ void xr_input_shutdown(void) {
|
|||||||
g_keyboard_drag.hand = -1;
|
g_keyboard_drag.hand = -1;
|
||||||
g_menu_drag.dragging = false;
|
g_menu_drag.dragging = false;
|
||||||
g_menu_drag.hand = -1;
|
g_menu_drag.hand = -1;
|
||||||
|
g_turn_ready = true;
|
||||||
|
g_last_move_ticks = 0;
|
||||||
|
g_pending_startup_recenter = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,36 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw);
|
|||||||
// frame.
|
// frame.
|
||||||
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad);
|
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad);
|
||||||
|
|
||||||
|
// Recenters the player so the game quad ("virtual screen") appears directly
|
||||||
|
// ahead of wherever they're currently really standing/facing - a pure,
|
||||||
|
// live "return to a sane view of the screen" action, unrelated to any
|
||||||
|
// stored/config value (unlike xr_session_set_player_transform()'s other
|
||||||
|
// callers, this never reads or writes questshock.config.json - it's driven
|
||||||
|
// entirely by the player's current real head pose, the same way
|
||||||
|
// xr_input_sync_and_draw()'s own menu re-center is). Called from the Main
|
||||||
|
// Menu tab's "Reset Position" button (via qs_config.c's cross-thread
|
||||||
|
// dispatch - see qs_config_request_reset_position()), but doesn't touch
|
||||||
|
// anything else qs_config.c owns; also called once automatically, from
|
||||||
|
// inside xr_input_sync_and_draw() itself, on whichever frame first has a
|
||||||
|
// valid head pose after startup (see g_pending_startup_recenter's own
|
||||||
|
// comment) - but only as a *fallback* default for when there's no
|
||||||
|
// complete saved player_position to fall back on instead (see
|
||||||
|
// xr_input_skip_startup_recenter() below), since a config-loaded value
|
||||||
|
// describes a transform relative to a *previous* session's real-space
|
||||||
|
// origin, which is only meaningful if that previous position was actually
|
||||||
|
// captured (LOCAL space's own origin isn't a stable point across
|
||||||
|
// launches). Returns false (no-op otherwise) if no valid head pose was
|
||||||
|
// available to recenter onto yet.
|
||||||
|
bool xr_input_recenter_player_to_screen(void);
|
||||||
|
|
||||||
|
// Called once, from qs_config_load_startup() (qs_config.c), when a
|
||||||
|
// complete saved player_position was actually loaded from
|
||||||
|
// questshock.config.json - cancels the pending automatic startup recenter
|
||||||
|
// above so it doesn't immediately override the value that was just
|
||||||
|
// applied. Safe to call regardless of whether xr_input_init() has even run
|
||||||
|
// yet (this only touches the plain file-local flag, no XR calls).
|
||||||
|
void xr_input_skip_startup_recenter(void);
|
||||||
|
|
||||||
void xr_input_shutdown(void);
|
void xr_input_shutdown(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "xr_overlay.h"
|
#include "xr_overlay.h"
|
||||||
|
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
|
#include <math.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -18,11 +19,33 @@
|
|||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||||
|
|
||||||
|
// Same constant (deliberately not <math.h>'s M_PI, which isn't used
|
||||||
|
// anywhere else in this codebase and isn't guaranteed by the C standard -
|
||||||
|
// only POSIX) as xr_session.c's own play-space rebuild math.
|
||||||
|
#define QS_DEG_TO_RAD (3.14159265358979323846f / 180.0f)
|
||||||
|
|
||||||
struct XrOverlay {
|
struct XrOverlay {
|
||||||
XrInstance instance; // for error logging only
|
XrInstance instance; // for error logging only
|
||||||
int width, height;
|
int width, height;
|
||||||
float distance_m, half_width_m, half_height_m;
|
float distance_m, half_width_m, half_height_m;
|
||||||
float center_x_m, center_y_m;
|
float center_x_m, center_y_m;
|
||||||
|
// The *authored* distance_m (config->distance_m at creation time),
|
||||||
|
// never touched again afterward by anything, including
|
||||||
|
// xr_overlay_set_pose() - kept separate from the live, mutable
|
||||||
|
// distance_m above because that one stops meaning "how far in front of
|
||||||
|
// the origin" once a rotated set_pose() call (the menu recenter)
|
||||||
|
// repurposes it as a raw base-space Z coordinate; reusing *that* as a
|
||||||
|
// "how far in front of me" magnitude on a later recenter would drift
|
||||||
|
// further with every reopen instead of staying at a stable distance -
|
||||||
|
// see xr_overlay_get_default_distance_m().
|
||||||
|
float default_distance_m;
|
||||||
|
// Rotation about the vertical axis, applied on top of center_x_m/
|
||||||
|
// center_y_m/distance_m (see xr_overlay_set_pose()) - 0 for every
|
||||||
|
// overlay except the menu launcher immediately after it's freshly
|
||||||
|
// reopened (xr_input.c re-centers it in front of wherever the player
|
||||||
|
// is currently facing, which needs this to keep the quad actually
|
||||||
|
// facing them instead of whatever direction it started out at).
|
||||||
|
float yaw_deg;
|
||||||
bool visible;
|
bool visible;
|
||||||
|
|
||||||
XrSwapchainState swapchain;
|
XrSwapchainState swapchain;
|
||||||
@@ -205,6 +228,7 @@ XrOverlay *xr_overlay_create(XrInstance instance, XrSession session, int64_t swa
|
|||||||
overlay->width = config->width;
|
overlay->width = config->width;
|
||||||
overlay->height = config->height;
|
overlay->height = config->height;
|
||||||
overlay->distance_m = config->distance_m;
|
overlay->distance_m = config->distance_m;
|
||||||
|
overlay->default_distance_m = config->distance_m;
|
||||||
overlay->half_width_m = config->width_m * 0.5f;
|
overlay->half_width_m = config->width_m * 0.5f;
|
||||||
overlay->half_height_m = config->width_m * 0.5f * (float)config->height / (float)config->width;
|
overlay->half_height_m = config->width_m * 0.5f * (float)config->height / (float)config->width;
|
||||||
overlay->center_x_m = config->default_center_x_m;
|
overlay->center_x_m = config->default_center_x_m;
|
||||||
@@ -281,9 +305,10 @@ void xr_overlay_set_visible(XrOverlay *overlay, bool visible) {
|
|||||||
bool xr_overlay_is_visible(const XrOverlay *overlay) { return overlay != NULL && overlay->visible; }
|
bool xr_overlay_is_visible(const XrOverlay *overlay) { return overlay != NULL && overlay->visible; }
|
||||||
|
|
||||||
void xr_overlay_get_quad_extent(const XrOverlay *overlay, float *center_x_m, float *center_y_m,
|
void xr_overlay_get_quad_extent(const XrOverlay *overlay, float *center_x_m, float *center_y_m,
|
||||||
float *distance_m, float *half_width_m, float *half_height_m) {
|
float *distance_m, float *half_width_m, float *half_height_m,
|
||||||
|
float *yaw_deg) {
|
||||||
if (overlay == NULL) {
|
if (overlay == NULL) {
|
||||||
*center_x_m = *center_y_m = *distance_m = *half_width_m = *half_height_m = 0.0f;
|
*center_x_m = *center_y_m = *distance_m = *half_width_m = *half_height_m = *yaw_deg = 0.0f;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*center_x_m = overlay->center_x_m;
|
*center_x_m = overlay->center_x_m;
|
||||||
@@ -291,6 +316,7 @@ void xr_overlay_get_quad_extent(const XrOverlay *overlay, float *center_x_m, flo
|
|||||||
*distance_m = overlay->distance_m;
|
*distance_m = overlay->distance_m;
|
||||||
*half_width_m = overlay->half_width_m;
|
*half_width_m = overlay->half_width_m;
|
||||||
*half_height_m = overlay->half_height_m;
|
*half_height_m = overlay->half_height_m;
|
||||||
|
*yaw_deg = overlay->yaw_deg;
|
||||||
}
|
}
|
||||||
|
|
||||||
void xr_overlay_set_position(XrOverlay *overlay, float center_x_m, float center_y_m) {
|
void xr_overlay_set_position(XrOverlay *overlay, float center_x_m, float center_y_m) {
|
||||||
@@ -300,6 +326,20 @@ void xr_overlay_set_position(XrOverlay *overlay, float center_x_m, float center_
|
|||||||
overlay->center_y_m = center_y_m;
|
overlay->center_y_m = center_y_m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void xr_overlay_set_pose(XrOverlay *overlay, float center_x_m, float center_y_m, float distance_m,
|
||||||
|
float yaw_deg) {
|
||||||
|
if (overlay == NULL)
|
||||||
|
return;
|
||||||
|
overlay->center_x_m = center_x_m;
|
||||||
|
overlay->center_y_m = center_y_m;
|
||||||
|
overlay->distance_m = distance_m;
|
||||||
|
overlay->yaw_deg = yaw_deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
float xr_overlay_get_default_distance_m(const XrOverlay *overlay) {
|
||||||
|
return overlay == NULL ? 0.0f : overlay->default_distance_m;
|
||||||
|
}
|
||||||
|
|
||||||
void xr_overlay_touch(XrOverlay *overlay, float u, float v, bool down) {
|
void xr_overlay_touch(XrOverlay *overlay, float u, float v, bool down) {
|
||||||
if (overlay == NULL)
|
if (overlay == NULL)
|
||||||
return;
|
return;
|
||||||
@@ -443,7 +483,17 @@ bool xr_overlay_render_and_build_layer(XrOverlay *overlay, XrCompositionLayerQua
|
|||||||
out_quad->subImage.swapchain = overlay->swapchain.swapchain;
|
out_quad->subImage.swapchain = overlay->swapchain.swapchain;
|
||||||
out_quad->subImage.imageRect.extent.width = overlay->swapchain.width;
|
out_quad->subImage.imageRect.extent.width = overlay->swapchain.width;
|
||||||
out_quad->subImage.imageRect.extent.height = overlay->swapchain.height;
|
out_quad->subImage.imageRect.extent.height = overlay->swapchain.height;
|
||||||
out_quad->pose.orientation.w = 1.0f;
|
// Rotation about the vertical axis only (see the struct field's own
|
||||||
|
// comment) - same R_y(-yaw_deg) quaternion construction as
|
||||||
|
// xr_session.c's own play-space rebuild, for the same reason: this
|
||||||
|
// quad's pose is expressed as "yaw_deg turned from its own local -Z",
|
||||||
|
// so a point/ray in that rotated frame maps back into the (unrotated)
|
||||||
|
// base space by rotating by -yaw_deg.
|
||||||
|
float half = -(overlay->yaw_deg * QS_DEG_TO_RAD) * 0.5f;
|
||||||
|
out_quad->pose.orientation.x = 0.0f;
|
||||||
|
out_quad->pose.orientation.y = sinf(half);
|
||||||
|
out_quad->pose.orientation.z = 0.0f;
|
||||||
|
out_quad->pose.orientation.w = cosf(half);
|
||||||
out_quad->pose.position.x = overlay->center_x_m;
|
out_quad->pose.position.x = overlay->center_x_m;
|
||||||
out_quad->pose.position.y = overlay->center_y_m;
|
out_quad->pose.position.y = overlay->center_y_m;
|
||||||
out_quad->pose.position.z = -overlay->distance_m;
|
out_quad->pose.position.z = -overlay->distance_m;
|
||||||
|
|||||||
@@ -58,18 +58,39 @@ void xr_overlay_toggle_visible(XrOverlay *overlay);
|
|||||||
void xr_overlay_set_visible(XrOverlay *overlay, bool visible);
|
void xr_overlay_set_visible(XrOverlay *overlay, bool visible);
|
||||||
bool xr_overlay_is_visible(const XrOverlay *overlay);
|
bool xr_overlay_is_visible(const XrOverlay *overlay);
|
||||||
|
|
||||||
// Local-space pose/size of the quad, valid regardless of visibility -
|
// Base-space pose/size of the quad, valid regardless of visibility - shared
|
||||||
// shared with xr_input.c's ray/quad hit-testing.
|
// with xr_input.c's ray/quad hit-testing. yaw_deg is rotation about the
|
||||||
|
// vertical axis (see xr_overlay_set_pose()) - 0 unless something has
|
||||||
|
// called that.
|
||||||
void xr_overlay_get_quad_extent(const XrOverlay *overlay, float *center_x_m, float *center_y_m,
|
void xr_overlay_get_quad_extent(const XrOverlay *overlay, float *center_x_m, float *center_y_m,
|
||||||
float *distance_m, float *half_width_m, float *half_height_m);
|
float *distance_m, float *half_width_m, float *half_height_m,
|
||||||
|
float *yaw_deg);
|
||||||
|
|
||||||
// Repositions the quad (world-space X/Y offset, same units/space as
|
// Repositions the quad (base-space X/Y offset, same units/space as
|
||||||
// xr_overlay_get_quad_extent()'s center_x_m/center_y_m) - e.g. driven by
|
// xr_overlay_get_quad_extent()'s center_x_m/center_y_m) - e.g. driven by
|
||||||
// xr_input.c while the user drags a panel's title bar. Distance from the
|
// xr_input.c while the user drags a panel's title bar. Distance from the
|
||||||
// viewer isn't adjustable this way - only left/right/up/down
|
// viewer and rotation aren't adjustable this way - only left/right/up/down
|
||||||
// repositioning, not push/pull.
|
// repositioning, not push/pull or turning - see xr_overlay_set_pose() for
|
||||||
|
// that.
|
||||||
void xr_overlay_set_position(XrOverlay *overlay, float center_x_m, float center_y_m);
|
void xr_overlay_set_position(XrOverlay *overlay, float center_x_m, float center_y_m);
|
||||||
|
|
||||||
|
// Full reposition: X/Y offset, distance ("push/pull"), and rotation about
|
||||||
|
// the vertical axis (degrees, same sign convention as
|
||||||
|
// xr_session_set_player_transform()'s yaw_deg) - used by xr_input.c to
|
||||||
|
// re-center the menu launcher in front of wherever the player currently
|
||||||
|
// is/facing each time it's freshly reopened, unlike
|
||||||
|
// xr_overlay_set_position()'s drag-only X/Y-at-fixed-distance-and-facing.
|
||||||
|
void xr_overlay_set_pose(XrOverlay *overlay, float center_x_m, float center_y_m, float distance_m,
|
||||||
|
float yaw_deg);
|
||||||
|
|
||||||
|
// The overlay's original, never-mutated distance_m (from XrOverlayConfig at
|
||||||
|
// creation time) - unlike xr_overlay_get_quad_extent()'s live distance_m,
|
||||||
|
// which xr_overlay_set_pose() repurposes as a raw base-space Z coordinate
|
||||||
|
// once any rotation is involved (see xr_overlay_set_pose()'s own comment),
|
||||||
|
// this stays a stable "how far in front" magnitude across repeated
|
||||||
|
// recenters. 0 for a NULL overlay.
|
||||||
|
float xr_overlay_get_default_distance_m(const XrOverlay *overlay);
|
||||||
|
|
||||||
// Forwards a hit on the quad (in the quad's own 0..1 u/v, top-left origin)
|
// Forwards a hit on the quad (in the quad's own 0..1 u/v, top-left origin)
|
||||||
// to the backing Java view as a synthetic touch down/up.
|
// to the backing Java view as a synthetic touch down/up.
|
||||||
void xr_overlay_touch(XrOverlay *overlay, float u, float v, bool down);
|
void xr_overlay_touch(XrOverlay *overlay, float u, float v, bool down);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "xr_session.h"
|
#include "xr_session.h"
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
#include <math.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -65,6 +66,26 @@ static XrInstance g_instance = XR_NULL_HANDLE;
|
|||||||
static XrSystemId g_system_id = XR_NULL_SYSTEM_ID;
|
static XrSystemId g_system_id = XR_NULL_SYSTEM_ID;
|
||||||
static XrSession g_session = XR_NULL_HANDLE;
|
static XrSession g_session = XR_NULL_HANDLE;
|
||||||
static XrSpace g_local_space = XR_NULL_HANDLE;
|
static XrSpace g_local_space = XR_NULL_HANDLE;
|
||||||
|
|
||||||
|
// Everything this app submits/locates (game/menu/keyboard/beam quads, hand
|
||||||
|
// and head poses) is expressed relative to g_play_space, not g_local_space
|
||||||
|
// directly - it's g_local_space translated/rotated by the inverse of the
|
||||||
|
// player's own virtual transform (see xr_session_set_player_transform()'s
|
||||||
|
// own comment in xr_session.h and xr_session_rebuild_play_space() below),
|
||||||
|
// so moving/turning the player moves/turns everything else relative to
|
||||||
|
// them without touching any actual pose-building or hit-testing math
|
||||||
|
// elsewhere in the app - it's all still "relative to the base space",
|
||||||
|
// which just happens to no longer be g_local_space itself.
|
||||||
|
static XrSpace g_play_space = XR_NULL_HANDLE;
|
||||||
|
static float g_player_x = 0.0f;
|
||||||
|
static float g_player_z = 0.0f;
|
||||||
|
static float g_player_yaw_deg = 0.0f;
|
||||||
|
// Set by xr_session_set_player_transform() (including before xr_init() has
|
||||||
|
// even run - see its own comment), cleared once xr_session_rebuild_play_space()
|
||||||
|
// actually rebuilds g_play_space from the current x/z/yaw_deg - checked
|
||||||
|
// once per frame in xr_frame_end() so a stick held at rest costs nothing.
|
||||||
|
static bool g_player_transform_dirty = false;
|
||||||
|
|
||||||
static XrSessionState g_session_state = XR_SESSION_STATE_UNKNOWN;
|
static XrSessionState g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||||
// Distinct from g_session_state - true from a successful xrBeginSession
|
// Distinct from g_session_state - true from a successful xrBeginSession
|
||||||
// until xrEndSession/loss/exit. Many runtimes only advance the *state*
|
// until xrEndSession/loss/exit. Many runtimes only advance the *state*
|
||||||
@@ -256,6 +277,65 @@ static EGLConfig xr_get_current_egl_config(EGLDisplay display, EGLContext contex
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define QS_DEG_TO_RAD (3.14159265358979323846f / 180.0f)
|
||||||
|
|
||||||
|
// Destroys/recreates g_play_space from g_local_space, offset by the
|
||||||
|
// inverse of the player's current virtual transform (g_player_x/z/yaw_deg) -
|
||||||
|
// see xr_session_set_player_transform()'s comment in xr_session.h for why
|
||||||
|
// this is the inverse rather than the transform itself. Given
|
||||||
|
// V = Translate(x, 0, z) * Rotate_Y(yaw_deg) (the player's own transform,
|
||||||
|
// "apparent pose in play space = V composed with real pose in local
|
||||||
|
// space"), the new space's poseInReferenceSpace must be V's inverse:
|
||||||
|
// orientation = Rotate_Y(-yaw_deg), position = Rotate_Y(-yaw_deg) * (-x, 0, -z).
|
||||||
|
// Called once, right after g_local_space itself is created (picking up
|
||||||
|
// whatever qs_config_load_startup() already seeded before xr_init() ever
|
||||||
|
// ran), and once per frame from xr_frame_end() whenever
|
||||||
|
// g_player_transform_dirty says the transform actually changed since the
|
||||||
|
// last rebuild - a plain stick-at-rest frame costs nothing beyond that one
|
||||||
|
// bool check. g_session must already exist.
|
||||||
|
static void xr_session_rebuild_play_space(void) {
|
||||||
|
if (g_play_space != XR_NULL_HANDLE) {
|
||||||
|
xrDestroySpace(g_play_space);
|
||||||
|
g_play_space = XR_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
float theta = g_player_yaw_deg * QS_DEG_TO_RAD;
|
||||||
|
float half = -theta * 0.5f;
|
||||||
|
float c = cosf(-theta), s = sinf(-theta);
|
||||||
|
float vx = -g_player_x, vz = -g_player_z;
|
||||||
|
|
||||||
|
XrReferenceSpaceCreateInfo spaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||||
|
spaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||||
|
spaceInfo.poseInReferenceSpace.orientation.x = 0.0f;
|
||||||
|
spaceInfo.poseInReferenceSpace.orientation.y = sinf(half);
|
||||||
|
spaceInfo.poseInReferenceSpace.orientation.z = 0.0f;
|
||||||
|
spaceInfo.poseInReferenceSpace.orientation.w = cosf(half);
|
||||||
|
spaceInfo.poseInReferenceSpace.position.x = vx * c + vz * s;
|
||||||
|
spaceInfo.poseInReferenceSpace.position.y = 0.0f;
|
||||||
|
spaceInfo.poseInReferenceSpace.position.z = -vx * s + vz * c;
|
||||||
|
xr_check(xrCreateReferenceSpace(g_session, &spaceInfo, &g_play_space),
|
||||||
|
"xrCreateReferenceSpace(play)");
|
||||||
|
|
||||||
|
g_player_transform_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void xr_session_set_player_transform(float x, float z, float yaw_deg) {
|
||||||
|
g_player_x = x;
|
||||||
|
g_player_z = z;
|
||||||
|
g_player_yaw_deg = yaw_deg;
|
||||||
|
g_player_transform_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void xr_session_get_player_transform(float *x, float *z, float *yaw_deg) {
|
||||||
|
*x = g_player_x;
|
||||||
|
*z = g_player_z;
|
||||||
|
*yaw_deg = g_player_yaw_deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
XrSpace xr_session_get_local_space(void) { return g_local_space; }
|
||||||
|
|
||||||
|
XrTime xr_session_get_predicted_display_time(void) { return g_predicted_display_time; }
|
||||||
|
|
||||||
static bool xr_create_instance_and_session(int game_width, int game_height) {
|
static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||||
jobject activity = (jobject)SDL_AndroidGetActivity();
|
jobject activity = (jobject)SDL_AndroidGetActivity();
|
||||||
@@ -342,6 +422,14 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
|||||||
"xrCreateReferenceSpace"))
|
"xrCreateReferenceSpace"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Picks up whatever qs_config_load_startup() already stored into
|
||||||
|
// g_player_x/z/yaw_deg via xr_session_set_player_transform() before
|
||||||
|
// this function ever ran (that call is pure state, no XR API calls, so
|
||||||
|
// it works fine even this early) - so a saved position is already in
|
||||||
|
// effect from the very first frame, not just after the player first
|
||||||
|
// moves a thumbstick.
|
||||||
|
xr_session_rebuild_play_space();
|
||||||
|
|
||||||
// Prefer a plain linear 8-bit format over GL_SRGB8_ALPHA8: the source
|
// Prefer a plain linear 8-bit format over GL_SRGB8_ALPHA8: the source
|
||||||
// SDL surface pixels are already sRGB-encoded (as ordinary 8-bit image
|
// SDL surface pixels are already sRGB-encoded (as ordinary 8-bit image
|
||||||
// data conventionally is) and get uploaded/sampled as plain linear
|
// data conventionally is) and get uploaded/sampled as plain linear
|
||||||
@@ -546,6 +634,14 @@ void xr_frame_end(void) {
|
|||||||
if (g_session == XR_NULL_HANDLE)
|
if (g_session == XR_NULL_HANDLE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Rebuilt before xr_input_sync_and_draw() below so this same frame's
|
||||||
|
// hit-testing and layer submission both see whatever the player's
|
||||||
|
// thumbstick input (or a config-driven "Reset Position"/Load
|
||||||
|
// Configuration) changed last frame - a no-op check on every frame
|
||||||
|
// nothing moved.
|
||||||
|
if (g_player_transform_dirty)
|
||||||
|
xr_session_rebuild_play_space();
|
||||||
|
|
||||||
// Syncs actions and hit-tests/dispatches each hand's ray against
|
// Syncs actions and hit-tests/dispatches each hand's ray against
|
||||||
// keyboard/menu/game quad (see xr_input.c) before the game swapchain
|
// keyboard/menu/game quad (see xr_input.c) before the game swapchain
|
||||||
// image is released below - this always runs, even when there's
|
// image is released below - this always runs, even when there's
|
||||||
@@ -554,7 +650,7 @@ void xr_frame_end(void) {
|
|||||||
// laser-beam quads are submitted as their own composition layers
|
// laser-beam quads are submitted as their own composition layers
|
||||||
// further down, not drawn into the game swapchain itself.
|
// further down, not drawn into the game swapchain itself.
|
||||||
if (xr_is_session_running())
|
if (xr_is_session_running())
|
||||||
xr_input_sync_and_draw(g_local_space, g_predicted_display_time,
|
xr_input_sync_and_draw(g_play_space, g_predicted_display_time,
|
||||||
g_have_acquired_game_image);
|
g_have_acquired_game_image);
|
||||||
|
|
||||||
if (g_have_acquired_game_image)
|
if (g_have_acquired_game_image)
|
||||||
@@ -572,7 +668,7 @@ void xr_frame_end(void) {
|
|||||||
// laser-beam quad while that hand's ray is aimed at the keyboard,
|
// laser-beam quad while that hand's ray is aimed at the keyboard,
|
||||||
// menu, or the game quad itself (see xr_input_get_beam_layer()).
|
// menu, or the game quad itself (see xr_input_get_beam_layer()).
|
||||||
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||||
gameQuad.space = g_local_space;
|
gameQuad.space = g_play_space;
|
||||||
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||||
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
||||||
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
||||||
@@ -590,7 +686,7 @@ void xr_frame_end(void) {
|
|||||||
|
|
||||||
XrCompositionLayerQuad menuQuad;
|
XrCompositionLayerQuad menuQuad;
|
||||||
if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &menuQuad)) {
|
if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &menuQuad)) {
|
||||||
menuQuad.space = g_local_space;
|
menuQuad.space = g_play_space;
|
||||||
menuQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
menuQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&menuQuad;
|
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&menuQuad;
|
||||||
}
|
}
|
||||||
@@ -598,7 +694,7 @@ void xr_frame_end(void) {
|
|||||||
XrCompositionLayerQuad keyboardQuad;
|
XrCompositionLayerQuad keyboardQuad;
|
||||||
if (g_frame_should_render &&
|
if (g_frame_should_render &&
|
||||||
xr_overlay_render_and_build_layer(g_keyboard_overlay, &keyboardQuad)) {
|
xr_overlay_render_and_build_layer(g_keyboard_overlay, &keyboardQuad)) {
|
||||||
keyboardQuad.space = g_local_space;
|
keyboardQuad.space = g_play_space;
|
||||||
keyboardQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
keyboardQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||||
// Lets the keyboard sit on screen translucently while playing
|
// Lets the keyboard sit on screen translucently while playing
|
||||||
// rather than fully hiding whatever's behind it - KeyboardOverlay
|
// rather than fully hiding whatever's behind it - KeyboardOverlay
|
||||||
@@ -618,7 +714,7 @@ void xr_frame_end(void) {
|
|||||||
XrCompositionLayerQuad beamQuad[2];
|
XrCompositionLayerQuad beamQuad[2];
|
||||||
for (int hand = 0; hand < 2; hand++) {
|
for (int hand = 0; hand < 2; hand++) {
|
||||||
if (xr_input_get_beam_layer(hand, &beamQuad[hand])) {
|
if (xr_input_get_beam_layer(hand, &beamQuad[hand])) {
|
||||||
beamQuad[hand].space = g_local_space;
|
beamQuad[hand].space = g_play_space;
|
||||||
beamQuad[hand].eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
beamQuad[hand].eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||||
beamQuad[hand].layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
beamQuad[hand].layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
||||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&beamQuad[hand];
|
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&beamQuad[hand];
|
||||||
@@ -643,6 +739,10 @@ void xr_shutdown(void) {
|
|||||||
xrDestroySpace(g_local_space);
|
xrDestroySpace(g_local_space);
|
||||||
g_local_space = XR_NULL_HANDLE;
|
g_local_space = XR_NULL_HANDLE;
|
||||||
|
|
||||||
|
if (g_play_space != XR_NULL_HANDLE)
|
||||||
|
xrDestroySpace(g_play_space);
|
||||||
|
g_play_space = XR_NULL_HANDLE;
|
||||||
|
|
||||||
// Before the session/instance they were created from.
|
// Before the session/instance they were created from.
|
||||||
xr_input_shutdown();
|
xr_input_shutdown();
|
||||||
xr_overlay_destroy(g_menu_overlay);
|
xr_overlay_destroy(g_menu_overlay);
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ bool xr_frame_begin(void);
|
|||||||
// launcher and keyboard overlay quads via xr_overlay_render_and_build_layer()
|
// launcher and keyboard overlay quads via xr_overlay_render_and_build_layer()
|
||||||
// for whichever of them is currently visible, and submits whichever of the
|
// for whichever of them is currently visible, and submits whichever of the
|
||||||
// three quads actually rendered this frame as composition layers
|
// three quads actually rendered this frame as composition layers
|
||||||
// positioned in front of the local reference space's origin, then ends the
|
// positioned in front of the play space's origin (see
|
||||||
// XR frame.
|
// xr_session_set_player_transform() above), then ends the XR frame.
|
||||||
void xr_frame_end(void);
|
void xr_frame_end(void);
|
||||||
|
|
||||||
void xr_shutdown(void);
|
void xr_shutdown(void);
|
||||||
|
|
||||||
// Local-space geometry of the single game quad xr_frame_end() submits
|
// Play-space geometry of the single game quad xr_frame_end() submits
|
||||||
// (distance in front of the local space origin, half-width/half-height,
|
// (distance in front of the play space origin, half-width/half-height,
|
||||||
// all in meters) - shared with xr_input.c's ray/quad hit-testing so the
|
// all in meters) - shared with xr_input.c's ray/quad hit-testing so the
|
||||||
// laser pointer always matches whatever's actually visible.
|
// laser pointer always matches whatever's actually visible.
|
||||||
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m);
|
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m);
|
||||||
@@ -98,6 +98,49 @@ float xr_session_get_frame_ms(void);
|
|||||||
float xr_session_get_cpu_utilization(void);
|
float xr_session_get_cpu_utilization(void);
|
||||||
float xr_session_get_gpu_utilization(void);
|
float xr_session_get_gpu_utilization(void);
|
||||||
|
|
||||||
|
// The player's virtual position/orientation - horizontal-plane only (x/z
|
||||||
|
// plus a yaw rotation about the vertical axis); height always comes from
|
||||||
|
// the real tracked HMD pose, never from this. Everything the app submits
|
||||||
|
// (game/menu/keyboard/beam quads) and every hand/head pose it locates is
|
||||||
|
// expressed relative to a second reference space, derived from the real
|
||||||
|
// LOCAL space by this transform's inverse (see xr_session.c's own
|
||||||
|
// xr_session_rebuild_play_space()) - so changing it moves/turns the player
|
||||||
|
// relative to those otherwise-fixed quads, letting them walk up to and
|
||||||
|
// square up with the game quad ("virtual screen") without needing any
|
||||||
|
// physical room-scale space to do it in.
|
||||||
|
//
|
||||||
|
// xr_session_set_player_transform() only stores the three floats (no XR
|
||||||
|
// calls) - safe to call before xr_init() has even run, which is how
|
||||||
|
// qs_config_load_startup() seeds the saved position before the OpenXR
|
||||||
|
// session/spaces exist yet (xr_init() picks it up when it creates its own
|
||||||
|
// spaces); also used live by xr_input.c's thumbstick locomotion and
|
||||||
|
// "Reset Position" recenter (xr_input_recenter_player_to_screen()) and
|
||||||
|
// qs_config.c's "Load Configuration", and read back by "Save Configuration"
|
||||||
|
// via the getter.
|
||||||
|
void xr_session_set_player_transform(float x, float z, float yaw_deg);
|
||||||
|
void xr_session_get_player_transform(float *x, float *z, float *yaw_deg);
|
||||||
|
|
||||||
|
// The real, physical tracking-origin reference space - used only by
|
||||||
|
// xr_input_recenter_player_to_screen() (xr_input.c), which needs the
|
||||||
|
// player's CURRENT real head pose (not one already bent through the
|
||||||
|
// existing play-space transform - see xr_session_set_player_transform()'s
|
||||||
|
// own comment above) to compute a brand new one. Every other consumer in
|
||||||
|
// this app (rendering, hit-testing) uses the play space instead - see
|
||||||
|
// xr_session_rebuild_play_space()'s own comment for why this one stays a
|
||||||
|
// separate, never-swapped-out space.
|
||||||
|
XrSpace xr_session_get_local_space(void);
|
||||||
|
|
||||||
|
// The most recent xrWaitFrame-predicted display time (xr_frame_begin()'s
|
||||||
|
// own g_predicted_display_time) - for the same reason as
|
||||||
|
// xr_session_get_local_space() above, xr_input_recenter_player_to_screen()
|
||||||
|
// needs a valid XrTime to locate a space with but isn't itself called from
|
||||||
|
// the normal per-frame xr_frame_end()/xr_input_sync_and_draw() call chain
|
||||||
|
// that already threads one through (it's triggered by a button press,
|
||||||
|
// dispatched via qs_config.c's cross-thread event queue instead). A frame
|
||||||
|
// or so of staleness doesn't matter for a one-shot recenter. 0 before the
|
||||||
|
// first frame has ever begun.
|
||||||
|
XrTime xr_session_get_predicted_display_time(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
private Button keyboardButton;
|
private Button keyboardButton;
|
||||||
private Button saveButton;
|
private Button saveButton;
|
||||||
private Button loadButton;
|
private Button loadButton;
|
||||||
|
private Button resetPositionButton;
|
||||||
private Button dismissButton;
|
private Button dismissButton;
|
||||||
private Button restartNowButton;
|
private Button restartNowButton;
|
||||||
|
|
||||||
@@ -213,6 +214,8 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
v -> nativeRequestSaveConfig());
|
v -> nativeRequestSaveConfig());
|
||||||
loadButton = addRowButton(buttonRow, Strings.get("load_configuration", currentLanguage),
|
loadButton = addRowButton(buttonRow, Strings.get("load_configuration", currentLanguage),
|
||||||
v -> nativeRequestLoadConfig());
|
v -> nativeRequestLoadConfig());
|
||||||
|
resetPositionButton = addRowButton(buttonRow, Strings.get("reset_position", currentLanguage),
|
||||||
|
v -> nativeRequestResetPosition());
|
||||||
mainMenuContent.addView(buttonRow, new LinearLayout.LayoutParams(
|
mainMenuContent.addView(buttonRow, new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
@@ -514,6 +517,7 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
keyboardButton.setText(Strings.get("keyboard", currentLanguage));
|
keyboardButton.setText(Strings.get("keyboard", currentLanguage));
|
||||||
saveButton.setText(Strings.get("save_configuration", currentLanguage));
|
saveButton.setText(Strings.get("save_configuration", currentLanguage));
|
||||||
loadButton.setText(Strings.get("load_configuration", currentLanguage));
|
loadButton.setText(Strings.get("load_configuration", currentLanguage));
|
||||||
|
resetPositionButton.setText(Strings.get("reset_position", currentLanguage));
|
||||||
dismissButton.setText(Strings.get("dismiss", currentLanguage));
|
dismissButton.setText(Strings.get("dismiss", currentLanguage));
|
||||||
restartNowButton.setText(Strings.get("restart_now", currentLanguage));
|
restartNowButton.setText(Strings.get("restart_now", currentLanguage));
|
||||||
mainMenuTabButton.setText(Strings.get("main_menu", currentLanguage));
|
mainMenuTabButton.setText(Strings.get("main_menu", currentLanguage));
|
||||||
@@ -574,6 +578,12 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
private static native void nativeRequestSetResolution(int resolutionIndex);
|
private static native void nativeRequestSetResolution(int resolutionIndex);
|
||||||
// Main Menu tab's Language row - same enqueue-only reasoning as above.
|
// Main Menu tab's Language row - same enqueue-only reasoning as above.
|
||||||
private static native void nativeRequestSetLanguage(int languageIndex);
|
private static native void nativeRequestSetLanguage(int languageIndex);
|
||||||
|
// Main Menu tab's "Reset Position" button - same enqueue-only reasoning
|
||||||
|
// as above; recenters the player so the game quad appears directly
|
||||||
|
// ahead of wherever they're currently standing/facing (xr_input.c's
|
||||||
|
// xr_input_recenter_player_to_screen()) - a live-only action, unrelated
|
||||||
|
// to questshock.config.json.
|
||||||
|
private static native void nativeRequestResetPosition();
|
||||||
|
|
||||||
// Read-only queries - safe to call directly, see qs_config.h's own
|
// Read-only queries - safe to call directly, see qs_config.h's own
|
||||||
// comment on each of these.
|
// comment on each of these.
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ final class Strings {
|
|||||||
"Konfiguration speichern");
|
"Konfiguration speichern");
|
||||||
put("load_configuration", "Load Configuration", "Charger la configuration",
|
put("load_configuration", "Load Configuration", "Charger la configuration",
|
||||||
"Konfiguration laden");
|
"Konfiguration laden");
|
||||||
|
put("reset_position", "Reset Position", "Réinitialiser la position",
|
||||||
|
"Position zurücksetzen");
|
||||||
put("main_menu", "Main Menu", "Menu principal", "Hauptmenü");
|
put("main_menu", "Main Menu", "Menu principal", "Hauptmenü");
|
||||||
put("video_options", "Video Options", "Options vidéo", "Videooptionen");
|
put("video_options", "Video Options", "Options vidéo", "Videooptionen");
|
||||||
put("dismiss", "Dismiss", "Fermer", "Schließen");
|
put("dismiss", "Dismiss", "Fermer", "Schließen");
|
||||||
|
|||||||
Reference in New Issue
Block a user