Compare commits
11 Commits
d6e438a541
..
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d8324d409e | |||
| c24b7f15cb | |||
| e1e6a006d4 | |||
| 5fe4cdf7f8 | |||
| 369555efc7 | |||
| 360a1f7e3d | |||
| c0619a55ab | |||
| 09280dd410 | |||
| c4fd4eb3dd | |||
| e0a2e1f4bf | |||
| 504ed3e1cb |
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
# Game assets are not included in the repo
|
# Game assets are not included in the repo
|
||||||
/res/assets/setup_system_shock_enhanced*
|
/res/assets/setup_system_shock_enhanced*
|
||||||
|
/res/assets/setup_system_shock_classic*
|
||||||
/res/assets/*.zip
|
/res/assets/*.zip
|
||||||
/res/assets/ss_ee/
|
/res/assets/ss_ee/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,450 @@
|
|||||||
|
# Persistent configuration (`questshock.config.json`)
|
||||||
|
|
||||||
|
QuestShock can load a persistent, human-editable configuration file at
|
||||||
|
startup, in addition to (and independent of) the engine's own `prefs.txt`.
|
||||||
|
|
||||||
|
- **Location**: `/sdcard/questshock/questshock.config.json` - the same
|
||||||
|
folder you already copy `res/data`/`res/sound` into (see README.md
|
||||||
|
section 5.1).
|
||||||
|
- **Format**: plain JSON, grouped by concern. Each top-level key is a
|
||||||
|
*group*; each key inside a group is one *option*.
|
||||||
|
- **Loading**: read automatically at app startup, before the splash screen,
|
||||||
|
if the file exists. If it doesn't, `questshock.config.default.json` (same
|
||||||
|
folder) is used instead - a copy bundled with the app, refreshed on every
|
||||||
|
launch, never overwriting your own saved `questshock.config.json`. If
|
||||||
|
neither file exists, the engine behaves as it did before this file
|
||||||
|
existed (driven by `prefs.txt` as usual).
|
||||||
|
- **Saving**: never automatic. In-headset, open the menu (controller menu
|
||||||
|
button) and press **Save Configuration** to write the file from your
|
||||||
|
current live settings, overwriting it if present. Press **Load
|
||||||
|
Configuration** to re-read the file and apply it without restarting the
|
||||||
|
app.
|
||||||
|
- **Live vs. restart-required**: every option below is tagged as either
|
||||||
|
*live-appliable* (applying it while already playing takes effect
|
||||||
|
immediately) or *restart-required*. Pressing **Load Configuration**
|
||||||
|
applies every live-appliable change straight away; if the file also
|
||||||
|
changes something restart-required, a dialog lists exactly which
|
||||||
|
option(s) need it and offers:
|
||||||
|
- **Dismiss** - keep playing with the live-appliable changes already
|
||||||
|
applied; the restart-required option(s) stay as they were.
|
||||||
|
- **Restart Now** - relaunches the app, which re-runs this same startup
|
||||||
|
load and so applies every group in the file, including the
|
||||||
|
restart-required ones.
|
||||||
|
|
||||||
|
(Every option currently documented below is live-appliable, so this
|
||||||
|
dialog won't actually appear yet - it's built in for options added in a
|
||||||
|
future version that do need a restart.)
|
||||||
|
- Unrecognized groups/keys are ignored (harmless if you're editing the file
|
||||||
|
in advance for a future version). An invalid value for a *specific*
|
||||||
|
option (see each option's possible values below) is logged and that one
|
||||||
|
option is skipped - every other valid option in the file still applies.
|
||||||
|
A file that isn't valid JSON at all is ignored entirely, with nothing
|
||||||
|
applied.
|
||||||
|
- `questshock.config.json` and `prefs.txt` are fully independent - loading
|
||||||
|
or saving one never reads or writes the other.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"video_options": {
|
||||||
|
"renderer": "opengl",
|
||||||
|
"resolution": "1024x768",
|
||||||
|
"initial_screen_width_m": 1.6,
|
||||||
|
"initial_screen_distance_m": 2.0,
|
||||||
|
"game_screen_width_m": 2.8,
|
||||||
|
"game_screen_distance_m": 2.0,
|
||||||
|
"screen_width_offset_m": 1.0,
|
||||||
|
"screen_width_step_m": 0.6,
|
||||||
|
"screen_distance_offset_m": 1.0,
|
||||||
|
"screen_distance_step_m": 0.5
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"language": "english"
|
||||||
|
},
|
||||||
|
"player_position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"z": 0.0,
|
||||||
|
"yaw_degrees": 0.0
|
||||||
|
},
|
||||||
|
"player_controls": {
|
||||||
|
"move_forward": "left_thumbstick_up",
|
||||||
|
"move_back": "left_thumbstick_down",
|
||||||
|
"strafe_left": "left_thumbstick_left",
|
||||||
|
"strafe_right": "left_thumbstick_right",
|
||||||
|
"turn_left": "right_thumbstick_left",
|
||||||
|
"turn_right": "right_thumbstick_right",
|
||||||
|
"jump": "left_thumbstick_click",
|
||||||
|
"mouse_click": "right_a",
|
||||||
|
"mouse_double_click": "right_b",
|
||||||
|
"stand": "right_thumbstick_up",
|
||||||
|
"prone": "right_thumbstick_down",
|
||||||
|
"map_view": "right_thumbstick_click",
|
||||||
|
"fire_weapon": "right_trigger"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Video Options (`video_options`)
|
||||||
|
|
||||||
|
### `renderer`
|
||||||
|
|
||||||
|
Which renderer the game uses to draw the 3D view.
|
||||||
|
|
||||||
|
- **Type**: enum
|
||||||
|
- **Possible values**: `"software"`, `"opengl"`
|
||||||
|
- **Restart required?**: No (live-appliable)
|
||||||
|
|
||||||
|
### `resolution`
|
||||||
|
|
||||||
|
The game's internal rendering resolution.
|
||||||
|
|
||||||
|
- **Type**: enum
|
||||||
|
- **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"`
|
||||||
|
- **Restart required?**: No (live-appliable)
|
||||||
|
|
||||||
|
### `initial_screen_width_m`, `initial_screen_distance_m`, `game_screen_width_m`, `game_screen_distance_m`
|
||||||
|
|
||||||
|
The virtual screen's physical width and distance from the player (both in
|
||||||
|
meters) - the game shows two independently configurable screen
|
||||||
|
"profiles", switched automatically based on what's currently on screen:
|
||||||
|
|
||||||
|
- **Initial Screen** (`initial_screen_*`) - the splash screen, intro
|
||||||
|
cutscenes, and the game's own main menu.
|
||||||
|
- **Game Screen** (`game_screen_*`) - actual gameplay (the 3D view,
|
||||||
|
automap, cyberspace, etc.).
|
||||||
|
|
||||||
|
Switching happens automatically - no player action needed - the instant
|
||||||
|
the game itself transitions between these (e.g. starting/loading a game,
|
||||||
|
or returning to the menu).
|
||||||
|
|
||||||
|
- **Type**: number (meters)
|
||||||
|
- **Possible values**: the in-headset Video Options tab offers 5 presets
|
||||||
|
each - widths `1.0`, `1.6` (the original, pre-this-feature default),
|
||||||
|
`2.2`, `2.8`, `3.4`; distances `1.0`, `1.5`, `2.0` (the original,
|
||||||
|
pre-this-feature default), `2.5`, `3.0` - but a hand-edited value
|
||||||
|
doesn't need to match a preset exactly (it's clamped to a sane range -
|
||||||
|
width `[0.2, 10.0]`, distance `[0.5, 10.0]` - rather than snapped or
|
||||||
|
rejected; the Video Options tab just shows no button selected for an
|
||||||
|
in-between value).
|
||||||
|
- **Restart required?**: No (live-appliable)
|
||||||
|
- **Note**: the screen is always flat - there's no curve option.
|
||||||
|
|
||||||
|
### `screen_width_offset_m`, `screen_width_step_m`, `screen_distance_offset_m`, `screen_distance_step_m`
|
||||||
|
|
||||||
|
Reshapes the 5 preset buttons the Video Options tab offers for width and
|
||||||
|
distance (above) - config-file-only, there's no in-headset control for
|
||||||
|
these. Shared by both **Initial Screen** and **Game Screen** (both
|
||||||
|
columns' buttons show the same 5 values per row) - this only changes
|
||||||
|
what the buttons offer, not either profile's current value.
|
||||||
|
|
||||||
|
Preset button `index` (0-4) shows `offset + step * index`. For example,
|
||||||
|
a distance offset of `0` and step of `2` shows buttons `0`, `2`, `4`,
|
||||||
|
`6`, `8`; the defaults below reproduce this feature's original launch
|
||||||
|
presets exactly.
|
||||||
|
|
||||||
|
- **Type**: number (meters)
|
||||||
|
- **Default values**: width offset `1.0`, step `0.6` (buttons `1.0`,
|
||||||
|
`1.6`, `2.2`, `2.8`, `3.4`); distance offset `1.0`, step `0.5` (buttons
|
||||||
|
`1.0`, `1.5`, `2.0`, `2.5`, `3.0`).
|
||||||
|
- **Restart required?**: No (live-appliable)
|
||||||
|
- **Note**: not clamped - an unusual offset/step (e.g. negative step) is
|
||||||
|
accepted as-is. A resulting preset value outside the current value's
|
||||||
|
own clamp range (see above) still clamps normally when that preset
|
||||||
|
button is actually pressed.
|
||||||
|
|
||||||
|
## General (`general`)
|
||||||
|
|
||||||
|
### `language`
|
||||||
|
|
||||||
|
The game's UI/text language - both the game's own resource strings and this
|
||||||
|
project's own menu/keyboard chrome text (title bar, buttons, etc.).
|
||||||
|
|
||||||
|
- **Type**: enum
|
||||||
|
- **Possible values**: `"english"`, `"french"`, `"german"`
|
||||||
|
- **Restart required?**: No (live-appliable) - applies the same way the
|
||||||
|
game's own in-game options menu applies a language change (reloads the
|
||||||
|
language-specific resource files and refreshes the inventory/MFD/side-icon
|
||||||
|
UI immediately).
|
||||||
|
- **Note**: French and German both need their own full resource pack
|
||||||
|
(`frnstrng.res`/`mfdfrn.res`, `gerstrng.res`/`mfdger.res` under
|
||||||
|
`res/data/`) - if any of those files aren't present, the switch is
|
||||||
|
refused (logged, `questshock.config.json` and the current language are
|
||||||
|
left unchanged) rather than leaving the game with no valid resource file
|
||||||
|
open. See README.md section 3 ("Game assets") for where `mfdfrn.res`/
|
||||||
|
`mfdger.res` come from - the GOG Enhanced Edition installer alone doesn't
|
||||||
|
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 live-only action, not a config
|
||||||
|
value: it moves you to the centre in front of the game quad, based on where
|
||||||
|
your head actually is right now. It resets only your *position*, not which
|
||||||
|
way you're facing, so the screen does not rotate to follow your gaze - press
|
||||||
|
it while looking off to one side and the screen stays off to the other side.
|
||||||
|
The automatic recenter at startup (when there's no saved position) does
|
||||||
|
face the screen towards where you're looking. Reset Position never reads or
|
||||||
|
writes `questshock.config.json`.
|
||||||
|
|
||||||
|
### `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)
|
||||||
|
|
||||||
|
## Player Controls (`player_controls`)
|
||||||
|
|
||||||
|
Maps each of 57 in-game actions to a physical controller input: 9
|
||||||
|
movement/click actions, 2 free-view actions, 11 cyberspace actions, and 35
|
||||||
|
for the engine's own keyboard hotkeys and firing (posture, save/load, weapon
|
||||||
|
select, MFD panels, etc. - see the hotkey table below). Some actions are bound by default (see the default-input
|
||||||
|
columns); the rest are unbound and can be bound on demand. Assign them in the
|
||||||
|
**Controller Options** menu tab: tap an action's binding button, then press
|
||||||
|
the button or stick direction to use ("Press a button…"); tap it again, or
|
||||||
|
wait 8 seconds, to cancel without changing it.
|
||||||
|
|
||||||
|
- **Type**: one enum-string field per action (below)
|
||||||
|
- **Restart required?**: No (live-appliable) - a rebind takes effect
|
||||||
|
immediately, and persists across restarts once saved (**Save
|
||||||
|
Configuration**, same as every other group here).
|
||||||
|
- **Not included here**: opening/closing the VR menu itself is a fixed,
|
||||||
|
non-remappable button (see "Menu button" below) - there's no
|
||||||
|
`menu_toggle` field, and the Controller Options tab never lets you
|
||||||
|
reassign it. This guarantees you can always reach this tab and press
|
||||||
|
**Reset to Defaults** to recover from any bad rebind, no matter what else
|
||||||
|
you've reassigned.
|
||||||
|
- **Reset to Defaults**: restores the "Default input" column below for
|
||||||
|
every action not overridden by a `player_controls` group in
|
||||||
|
`questshock.config.default.json` - if that file defines one (it doesn't,
|
||||||
|
out of the box), the values it specifies are applied on top of the table
|
||||||
|
below instead, so you can customize what "reset" means without a rebuild.
|
||||||
|
This is independent of your own `questshock.config.json` - resetting
|
||||||
|
doesn't read or write that file, just the live bindings (**Save
|
||||||
|
Configuration** afterward to persist them).
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
| Action key | Default input | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `move_forward` | `left_thumbstick_up` | Walk forward |
|
||||||
|
| `move_back` | `left_thumbstick_down` | Walk backward |
|
||||||
|
| `strafe_left` | `left_thumbstick_left` | Step sideways, left |
|
||||||
|
| `strafe_right` | `left_thumbstick_right` | Step sideways, right |
|
||||||
|
| `turn_left` | `right_thumbstick_left` | Turn left |
|
||||||
|
| `turn_right` | `right_thumbstick_right` | Turn right |
|
||||||
|
| `jump` | `left_thumbstick_click` | Jump |
|
||||||
|
| `mouse_click` | `right_a` | Click - operates the keyboard/game quad's cursor, whichever the pointing hand's ray is over |
|
||||||
|
| `mouse_double_click` | `right_b` | Double-click - game quad only (e.g. using an inventory item); has no effect over the menu or keyboard |
|
||||||
|
| `free_left_thumbstick_view` | `left_squeeze` | Hold to look around with the left thumbstick (see below) |
|
||||||
|
| `free_right_thumbstick_view` | `none` | Hold to look around with the right thumbstick (see below) |
|
||||||
|
|
||||||
|
Movement actions bound to a thumbstick direction are suppressed while the
|
||||||
|
VR menu is open, since the same physical stick already snap-turns/smooth-
|
||||||
|
moves the player in that state (see `player_position` above) - this is the
|
||||||
|
one case where a binding's effect depends on whether the menu is open, and
|
||||||
|
it only ever affects the 8 thumbstick-direction inputs below.
|
||||||
|
|
||||||
|
**Free view** (`free_left_thumbstick_view`, `free_right_thumbstick_view`) is
|
||||||
|
the engine's mouse look driven by a thumbstick. While the bound input is held,
|
||||||
|
that stick looks around instead of doing its normal job: left/right turns the
|
||||||
|
view, up/down looks up and down, in proportion to how far the stick is pushed.
|
||||||
|
The stick's own direction bindings (move, strafe, turn, stand/prone) are
|
||||||
|
suspended while it is held, and the cursor stays centred in the view. Release
|
||||||
|
the input to return to normal. It has no effect while the VR menu is open.
|
||||||
|
|
||||||
|
### Cyberspace actions
|
||||||
|
|
||||||
|
Cyberspace has its own set of movement actions, active only while the player
|
||||||
|
is in cyberspace. While in cyberspace the normal movement actions
|
||||||
|
(`move_*`, `strafe_*`, `turn_*`, `jump`, `stand`, `crouch`, `prone`) are
|
||||||
|
inactive, and the cyberspace actions are inactive everywhere else. Each set
|
||||||
|
has its own bindings, so one input can be bound in both (for example the left
|
||||||
|
stick up is `move_forward` on foot and `cyber_climb` in cyberspace);
|
||||||
|
the Controller Options tab only reports a conflict between actions that can
|
||||||
|
be active at the same time. Free view, clicking and firing work in both.
|
||||||
|
|
||||||
|
Pitch and bank (`cyber_climb`, `cyber_dive`, `cyber_bank_*` and the four
|
||||||
|
diagonals) are proportional when bound to a thumbstick direction: pushing the
|
||||||
|
stick further turns faster, from 20% of full speed just past the trigger point
|
||||||
|
to 60% at full deflection. Bound to a button they run at full speed. Thrust and
|
||||||
|
roll are always full speed.
|
||||||
|
|
||||||
|
| Action key | Default input | Keyboard equivalent | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `cyber_thrust` | `right_thumbstick_up` | `s` | Thrust forward |
|
||||||
|
| `cyber_climb` | `left_thumbstick_up` | `w` | Pitch up |
|
||||||
|
| `cyber_dive` | `left_thumbstick_down` | `x` | Pitch down |
|
||||||
|
| `cyber_bank_left` | `left_thumbstick_left` | `a` | Bank left |
|
||||||
|
| `cyber_bank_right` | `left_thumbstick_right` | `d` | Bank right |
|
||||||
|
| `cyber_roll_left` | `right_thumbstick_left` | `e` | Roll left |
|
||||||
|
| `cyber_roll_right` | `right_thumbstick_right` | `q` | Roll right |
|
||||||
|
| `cyber_climb_left` | `none` | Keypad 7 | Pitch up and bank left |
|
||||||
|
| `cyber_climb_right` | `none` | Keypad 9 | Pitch up and bank right |
|
||||||
|
| `cyber_dive_left` | `none` | Keypad 1 | Pitch down and bank left |
|
||||||
|
| `cyber_dive_right` | `none` | Keypad 3 | Pitch down and bank right |
|
||||||
|
|
||||||
|
`mouse_click` additionally behaves differently depending on the menu:
|
||||||
|
while the VR menu is open, its "click" is always whichever hand's trigger
|
||||||
|
is used to interact with the menu (so you can never lock yourself out of
|
||||||
|
this settings screen by rebinding `mouse_click` elsewhere); once the menu
|
||||||
|
is closed, both the on-screen keyboard (if it's still open) and the game
|
||||||
|
quad use your actual `mouse_click` binding. The default (`right_a`) is a
|
||||||
|
specific button on the right controller, so only the right hand's ray clicks;
|
||||||
|
bind the `trigger` identifier (see below) for either hand to click.
|
||||||
|
|
||||||
|
### Hotkey actions
|
||||||
|
|
||||||
|
The engine's own keyboard hotkeys (`HotKeyLookup[]` in `Prefs.c`) plus
|
||||||
|
firing, as 35 bindable actions. `stand` (`right_thumbstick_up`), `prone`
|
||||||
|
(`right_thumbstick_down`), `map_view` (`right_thumbstick_click`) and
|
||||||
|
`fire_weapon` (`right_trigger`) are bound by default; the rest are unbound
|
||||||
|
(`none`). Each fires once per press, like pressing the key on a real
|
||||||
|
keyboard (there is no hold behavior).
|
||||||
|
|
||||||
|
| Action key | Keyboard equivalent | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `stand` | `t` | Stand up |
|
||||||
|
| `crouch` | `g` | Crouch |
|
||||||
|
| `prone` | `b` | Go prone |
|
||||||
|
| `pause` | `p` | Pause the game |
|
||||||
|
| `full_view` | Ctrl+F | Switch to the fullscreen view loop |
|
||||||
|
| `normal_view` | Ctrl+D | Switch to the normal gameplay view |
|
||||||
|
| `map_view` | Ctrl+A | Switch to the automap view |
|
||||||
|
| `save_game` | Ctrl+S | Save the game |
|
||||||
|
| `load_game` | Ctrl+L | Load the game |
|
||||||
|
| `select_grenade` | Ctrl+' | Select the next grenade type |
|
||||||
|
| `arm_grenade` | Alt+' | Arm the selected grenade |
|
||||||
|
| `select_drug` | Ctrl+; | Select the next drug/patch |
|
||||||
|
| `use_drug` | Alt+; | Use the selected drug/patch |
|
||||||
|
| `toggle_online_help` | Ctrl+H | Toggle the online help overlay |
|
||||||
|
| `show_help` | Alt+O | Show the help screen |
|
||||||
|
| `toggle_music` | Ctrl+M | Toggle music |
|
||||||
|
| `quit` | Ctrl+Q | Quit the game - opt-in only; nothing binds this by default |
|
||||||
|
| `cycle_weapon_next` | Tab | Ready the next weapon |
|
||||||
|
| `cycle_weapon_prev` | Shift+Tab | Ready the previous weapon |
|
||||||
|
| `cycle_detail` | Ctrl+1 | Cycle the graphics detail level |
|
||||||
|
| `reload_weapon_primary` | Ctrl+Backspace | Reload the current weapon's primary ammo |
|
||||||
|
| `reload_weapon_secondary` | Alt+Backspace | Reload the current weapon's secondary ammo |
|
||||||
|
| `hud_color` | Alt+H | Cycle the HUD color scheme |
|
||||||
|
| `clear_fullscreen` | Backspace | Close a fullscreen view (e.g. a datapad or email) |
|
||||||
|
| `mfd_left_1`-`mfd_left_5` | F1-F5 | Set the left MFD panel to Weapon/Item/Map/Target/Info |
|
||||||
|
| `mfd_right_1`-`mfd_right_5` | F6-F10 | Set the right MFD panel to Weapon/Item/Map/Target/Info |
|
||||||
|
| `fire_weapon` | Enter | Fire the readied weapon at the cursor (the engine's default fire key). Clicking in the 3D view with `mouse_click` also fires; this is a separate button for it |
|
||||||
|
|
||||||
|
**Not exposed** (other `HotKeyLookup[]` entries):
|
||||||
|
- `keypad 0`-`keypad 9`: a controller button per digit is not useful; use the
|
||||||
|
on-screen keyboard.
|
||||||
|
- `toggle_freelook` (F): the free-view actions cover mouse look.
|
||||||
|
- The 4 `cheat_*` entries: debug cheats.
|
||||||
|
- `mac_help` (disabled) and `audiolog_cancel` (its build flag is not
|
||||||
|
defined): they are not real hotkeys in this build.
|
||||||
|
- `toggle_opengl` (Ctrl+G): the Video Options tab's Renderer buttons already
|
||||||
|
toggle the same setting.
|
||||||
|
- The 10 numeric hardware-item hotkeys (bio scan, fullscreen, 360 view,
|
||||||
|
lantern, shield, infrared, nav unit, data reader, booster, jumpjets): their
|
||||||
|
registered keys never match a real keypress in the engine.
|
||||||
|
|
||||||
|
### Raw input identifiers
|
||||||
|
|
||||||
|
Every raw physical input available to bind an action to, with its generic
|
||||||
|
OpenXR interaction-profile component name and its Meta Quest Touch control
|
||||||
|
name. Two symmetric identifiers (`trigger`, `squeeze`) are also accepted, but
|
||||||
|
only for `mouse_click`/`mouse_double_click`: they mean "either hand's own
|
||||||
|
trigger/grip, whichever hand is pointing". Every other identifier below
|
||||||
|
refers to one specific control.
|
||||||
|
|
||||||
|
| Identifier | OpenXR component (Touch) | Meta Quest control |
|
||||||
|
|---|---|---|
|
||||||
|
| `left_trigger` | `/user/hand/left/input/trigger/value` | Left Trigger |
|
||||||
|
| `right_trigger` | `/user/hand/right/input/trigger/value` | Right Trigger |
|
||||||
|
| `left_squeeze` | `/user/hand/left/input/squeeze/value` | Left Grip |
|
||||||
|
| `right_squeeze` | `/user/hand/right/input/squeeze/value` | Right Grip |
|
||||||
|
| `left_x` | `/user/hand/left/input/x/click` | X Button |
|
||||||
|
| `left_y` | `/user/hand/left/input/y/click` | Y Button |
|
||||||
|
| `right_a` | `/user/hand/right/input/a/click` | A Button |
|
||||||
|
| `right_b` | `/user/hand/right/input/b/click` | B Button |
|
||||||
|
| `left_thumbstick_click` | `/user/hand/left/input/thumbstick/click` | Left Thumbstick Click |
|
||||||
|
| `right_thumbstick_click` | `/user/hand/right/input/thumbstick/click` | Right Thumbstick Click |
|
||||||
|
| `left_thumbstick_up/down/left/right` | `/user/hand/left/input/thumbstick` (digitized) | Left Thumbstick, tilted |
|
||||||
|
| `right_thumbstick_up/down/left/right` | `/user/hand/right/input/thumbstick` (digitized) | Right Thumbstick, tilted |
|
||||||
|
| `trigger` | either hand's trigger | Either Trigger (`mouse_click`/`mouse_double_click` only) |
|
||||||
|
| `squeeze` | either hand's grip | Either Grip (`mouse_click`/`mouse_double_click` only) |
|
||||||
|
| `none` | - | Unbound - the action never fires |
|
||||||
|
|
||||||
|
**Menu button** (left controller, Touch/Vive/generic alike) is not in this
|
||||||
|
list - it always opens/closes the VR menu and can never be reassigned.
|
||||||
|
|
||||||
|
**Other supported controllers**: HTC Vive wands have no A/B/X/Y and no
|
||||||
|
analog grip - `left_x`/`left_y`/`right_a`/`right_b` simply never fire on
|
||||||
|
that profile, and `left/right_squeeze` are a plain click rather than
|
||||||
|
analog. Their trackpad stands in for the thumbstick identifiers above
|
||||||
|
(same digitization, just a trackpad instead of a stick). A bare-minimum
|
||||||
|
OpenXR controller (`khr/simple_controller`, used as a fallback when
|
||||||
|
neither Touch nor Vive bindings are suggested) only has `left_trigger`/
|
||||||
|
`right_trigger` (via its `select/click` component) and the menu button -
|
||||||
|
every other identifier above simply never fires on it, which is why the
|
||||||
|
Controller Options tab shows a note and hides irrelevant rows when this
|
||||||
|
profile is active.
|
||||||
|
|
||||||
|
### Controller profile files (`controller_profiles/`)
|
||||||
|
|
||||||
|
The Controller Options tab shows a labeled illustration of whichever
|
||||||
|
controller is actually active, driven by `res/controller_profiles/*.json`
|
||||||
|
(bundled with the app, copied to `/sdcard/questshock/controller_profiles/`
|
||||||
|
on every launch, refreshing the known bundled files without touching any
|
||||||
|
extra ones you add there yourself). Each file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Meta Quest Touch",
|
||||||
|
"interaction_profile": "/interaction_profiles/oculus/touch_controller",
|
||||||
|
"image": "touch.png",
|
||||||
|
"controls": [
|
||||||
|
{"id": "right_b", "label": "B", "anchor_x": 0.76, "anchor_y": 0.22}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `interaction_profile` - the OpenXR interaction profile path this file
|
||||||
|
describes; matched against whichever one is actually bound at runtime.
|
||||||
|
Falls back to `generic.json` if nothing matches.
|
||||||
|
- `image` - filename (same folder) of the controller illustration.
|
||||||
|
- `controls` - one entry per labeled point on that illustration: `id` is a
|
||||||
|
raw input identifier from the table above (thumbstick directions share
|
||||||
|
one `left_thumbstick`/`right_thumbstick` entry, since they're the same
|
||||||
|
physical stick); `anchor_x`/`anchor_y` are 0-1 fractions of the image for
|
||||||
|
positioning that label's pointer line.
|
||||||
|
|
||||||
|
This is presentation data only - adding real support for a new controller
|
||||||
|
still needs matching bindings in the app itself; a profile file with no
|
||||||
|
corresponding code-level support just never gets matched.
|
||||||
|
|
||||||
|
### Fatal startup error
|
||||||
|
|
||||||
|
If the OpenXR runtime rejects any of the three supported controller
|
||||||
|
profiles' bindings at launch (an app bug, not something you did), a
|
||||||
|
non-cancelable dialog explains this and exits - this should never happen
|
||||||
|
in a normal build.
|
||||||
@@ -90,6 +90,21 @@ package`/`make apk` pick it up the same way either way. This one copy of
|
|||||||
game assets feeds both the desktop build and the Android/Quest build
|
game assets feeds both the desktop build and the Android/Quest build
|
||||||
below.
|
below.
|
||||||
|
|
||||||
|
**Note on French/German**: the GOG Enhanced Edition installer only ships
|
||||||
|
English MFD (Multi-Function Display - the small in-game screens showing
|
||||||
|
the map, inventory, etc.) art resources (`mfdart.res`) - it doesn't include
|
||||||
|
`mfdfrn.res`/`mfdger.res`, which French/German need in addition to their
|
||||||
|
own text resources (`frnstrng.res`/`gerstrng.res`, which the installer
|
||||||
|
*does* include). Without those two files, switching to French/German (via
|
||||||
|
`questshock.config.json`'s `general.language` or the in-headset menu's
|
||||||
|
Language row - see [CONFIG.md](CONFIG.md)) is refused with an in-headset
|
||||||
|
popup rather than applied, since doing so anyway used to crash the game. If
|
||||||
|
you also own **System Shock: Classic** on gog.com (which does include both
|
||||||
|
files), drop its offline installer into `res/assets/` alongside the
|
||||||
|
Enhanced Edition one and `extract_assets.sh` pulls them out of it too,
|
||||||
|
entirely optionally - it still runs fine with just the Enhanced Edition
|
||||||
|
installer.
|
||||||
|
|
||||||
## 4. Desktop build
|
## 4. Desktop build
|
||||||
|
|
||||||
Builds and runs Questshock natively on the desktop (your dev machine, or
|
Builds and runs Questshock natively on the desktop (your dev machine, or
|
||||||
@@ -270,6 +285,11 @@ debugging enabled.)
|
|||||||
`/sdcard/questshock/res/`.
|
`/sdcard/questshock/res/`.
|
||||||
4. Launch it again.
|
4. Launch it again.
|
||||||
|
|
||||||
|
`/sdcard/questshock/` also holds an optional persistent settings file,
|
||||||
|
`questshock.config.json`, loaded automatically at startup and saved/loaded
|
||||||
|
from the in-headset menu - see [CONFIG.md](CONFIG.md) for the documented
|
||||||
|
options.
|
||||||
|
|
||||||
### 5.2. Building natively in Android Studio
|
### 5.2. Building natively in Android Studio
|
||||||
|
|
||||||
`make apk` always compiles the engine and links the APK inside the
|
`make apk` always compiles the engine and links the APK inside the
|
||||||
@@ -383,10 +403,14 @@ each one does:
|
|||||||
emulation) for real GLES-native equivalents: a custom `pointSize`
|
emulation) for real GLES-native equivalents: a custom `pointSize`
|
||||||
uniform, a shader-side alpha discard, and native point-sprite
|
uniform, a shader-side alpha discard, and native point-sprite
|
||||||
rasterization.
|
rasterization.
|
||||||
- **`05-android-audio-driver.patch`** - forces SDL2's older OpenSL ES
|
- **`05-android-audio-driver.patch`** - forces SDL2's OpenSL ES audio
|
||||||
audio backend instead of AAudio, because AAudio only allows one open
|
backend instead of AAudio via
|
||||||
playback device and the engine opens two (cutscene audio plus
|
`SDL_SetHintWithPriority(..., SDL_HINT_OVERRIDE)` (so nothing else can
|
||||||
`Mix_OpenAudio` for SFX/MIDI).
|
take precedence and silently leave AAudio selected), and logs the
|
||||||
|
actual `SDL_GetCurrentAudioDriver()` result right after `SDL_Init()`
|
||||||
|
as a standing on-device diagnostic. Does not by itself fix SFX/MIDI
|
||||||
|
playback - see `16-android-audio-single-device.patch` and README 5.5.3
|
||||||
|
for the actual cause and fix.
|
||||||
- **`06-android-resize-event.patch`** - makes the engine react to
|
- **`06-android-resize-event.patch`** - makes the engine react to
|
||||||
`SDL_WINDOWEVENT_RESIZED`, not just `SIZE_CHANGED`, since Android's SDL
|
`SDL_WINDOWEVENT_RESIZED`, not just `SIZE_CHANGED`, since Android's SDL
|
||||||
backend only ever sends the former for surface-driven resizes.
|
backend only ever sends the former for surface-driven resizes.
|
||||||
@@ -398,6 +422,10 @@ each one does:
|
|||||||
`SDL_SetWindowFullscreen`/`SetWindowSize`/`SetWindowPosition` calls on
|
`SDL_SetWindowFullscreen`/`SetWindowSize`/`SetWindowPosition` calls on
|
||||||
Android, since there's no real desktop-style window to resize and
|
Android, since there's no real desktop-style window to resize and
|
||||||
calling them desyncs SDL's cached window size from the real surface.
|
calling them desyncs SDL's cached window size from the real surface.
|
||||||
|
Also calls `opengl_resize()` directly in `ChangeScreenSize()`
|
||||||
|
(see README 5.5.6) - skipping `SDL_SetWindowSize()` means no native
|
||||||
|
resize event ever fires on Android to trigger it when the in-game
|
||||||
|
resolution changes, so it would otherwise never re-run there.
|
||||||
- **`10-android-openxr-cmake.patch`** - wires up the OpenXR loader plus
|
- **`10-android-openxr-cmake.patch`** - wires up the OpenXR loader plus
|
||||||
EGL as link/include dependencies for the CMake build.
|
EGL as link/include dependencies for the CMake build.
|
||||||
- **`11-android-openxr-present.patch`** - the core OpenXR present-path
|
- **`11-android-openxr-present.patch`** - the core OpenXR present-path
|
||||||
@@ -434,6 +462,77 @@ each one does:
|
|||||||
inherited non-default unpack state left behind elsewhere in the frame
|
inherited non-default unpack state left behind elsewhere in the frame
|
||||||
and misread every row. Fixed by resetting both to the packed defaults
|
and misread every row. Fixed by resetting both to the packed defaults
|
||||||
immediately before this upload.
|
immediately before this upload.
|
||||||
|
- **`15-android-audio-error-detail.patch`** - `snd_start_digital()`'s
|
||||||
|
`Mix_OpenAudio()` failure only logged a fixed string, not the actual
|
||||||
|
reason. Adds `SDL_GetError()` to the log line so the next on-device
|
||||||
|
failure reports the real underlying error.
|
||||||
|
- **`16-android-audio-single-device.patch`** - fixes SFX/MIDI music not
|
||||||
|
playing (see README 5.5.3). `snd_start_digital()` opened two SDL audio
|
||||||
|
devices: a raw `SDL_OpenAudioDevice()` for cutscene/audiolog audio and
|
||||||
|
a second one via `Mix_OpenAudio()` for SFX/MIDI - SDL's Android audio
|
||||||
|
backends only ever allow one open non-capture device, so the second
|
||||||
|
open always failed. Removes the raw device entirely and feeds cutscene/
|
||||||
|
audiolog PCM into the `Mix_OpenAudio()` device's output instead, via
|
||||||
|
`Mix_SetPostMix()` (mixing in rather than overwriting, so it doesn't
|
||||||
|
silence any SFX/MIDI music playing at the same time). `cutsloop.c`'s
|
||||||
|
and `audiolog.c`'s `SDL_PauseAudioDevice()` calls (no longer meaningful
|
||||||
|
without a dedicated device) are removed; the cutscene-audio-stream
|
||||||
|
pointer swaps they guarded are now protected by `SDL_LockAudio()`/
|
||||||
|
`SDL_UnlockAudio()` instead, since SDL_mixer's audio thread now reads
|
||||||
|
that pointer continuously via the post-mix hook.
|
||||||
|
- **`17-android-native-midi-unsupported.patch`** - `NativeMidiInit()` in
|
||||||
|
`MusicDevice.c` has real implementations only under `#ifdef WIN32` and
|
||||||
|
`#elif defined(USE_ALSA)`; on every other platform (Android included)
|
||||||
|
it fell through to `return 0` (success) without ever opening a device,
|
||||||
|
so a `midi-backend` preference of "Native" (see README 5.5.4) played
|
||||||
|
silently forever instead of falling back. Adds an `#else` branch that
|
||||||
|
returns failure and logs a `WARN`, so `InitDecXMI()`'s existing
|
||||||
|
fallback-to-dummy-driver logic actually triggers.
|
||||||
|
- **`18-android-opengl-immediate-mode-attribs.patch`** - fixes a black (and,
|
||||||
|
after this patch's first revision, flat-colored/untextured) screen when
|
||||||
|
switching to the OpenGL/3D render mode in-game (see README 5.5.5). Every
|
||||||
|
`glBegin(GL_TRIANGLE_STRIP)`-based draw in `OpenGL.cc` -
|
||||||
|
`opengl_swap_and_restore()`'s screen-composite quad,
|
||||||
|
`opengl_light_tmap()` (every textured wall/floor polygon, via
|
||||||
|
`draw_vertex()`), `opengl_bitmap()` (sprites), and `opengl_draw_poly()`
|
||||||
|
(flat/gouraud polygons, also via `draw_vertex()`) - used
|
||||||
|
`glVertexAttrib2f`/`glVertexAttrib1f`/`glVertex3f` immediate mode to set
|
||||||
|
per-vertex texcoords/light, which gl4es only captures per-vertex for
|
||||||
|
attribute 0 (position); custom attributes end up constant for the whole
|
||||||
|
draw call. Adds `android_imm_begin()`/`android_imm_vertex()`/
|
||||||
|
`android_imm_end()`, a small shared immediate-mode-emulation shim that
|
||||||
|
buffers vertices and flushes them as one real `glDrawArrays()` call, and
|
||||||
|
switches every one of those call sites to it, Android-only. The
|
||||||
|
starfield (`opengl_begin_stars()`/`opengl_draw_star()`/
|
||||||
|
`opengl_end_stars()`, `GL_POINTS` rather than `GL_TRIANGLE_STRIP`, up to
|
||||||
|
`NUM_STARS`=1000 per frame) had the same bug for its per-star brightness
|
||||||
|
and gets its own similarly-shaped `android_stars_begin()`/
|
||||||
|
`android_stars_vertex()`/`android_stars_end()` buffer. Also skips the
|
||||||
|
unconditional `SDL_GL_MakeCurrent(window, context)` call every
|
||||||
|
rendering function in this file had at its top, Android-only - a
|
||||||
|
worthwhile cleanup on its own merits, but *not* the source of the
|
||||||
|
persistent per-frame `GL_INVALID_OPERATION` (1282) logcat spam during
|
||||||
|
3D gameplay that this was first suspected to be (see README 5.5.8 for
|
||||||
|
that bug's actual root cause and fix, in patch 20 below).
|
||||||
|
- **`19-android-fix-hackcam-opengl-frame.patch`** - `fr_start_view()`/
|
||||||
|
`fr_send_view()` (`frsetup.c`) called `opengl_start_frame()`/
|
||||||
|
`opengl_end_frame()` again for hack-camera security-monitor renders
|
||||||
|
(which target a private software bitmap, not the GL-backed screen),
|
||||||
|
since `should_opengl_swap()` doesn't exclude them the way
|
||||||
|
`use_opengl()` already does. Adds the same exclusion to both call
|
||||||
|
sites (see README 5.5.8).
|
||||||
|
- **`20-android-no-default-framebuffer.patch`** - the actual fix for the
|
||||||
|
persistent per-frame `GL_INVALID_OPERATION` (1282) in README 5.5.8:
|
||||||
|
`opengl_end_frame()`'s `BindFrameBuffer(nullptr)` bind to framebuffer/
|
||||||
|
renderbuffer id 0 has no valid target on Android (real presentation
|
||||||
|
goes through the OpenXR swapchain's own FBOs, set up separately in
|
||||||
|
`xr_swapchain.c`) and is also functionally dead there regardless -
|
||||||
|
skipped entirely, Android-only.
|
||||||
|
- **`21-android-config.patch`** - hooks up the persistent JSON config file
|
||||||
|
(see [CONFIG.md](CONFIG.md), `qs_config.c`): one call to
|
||||||
|
`qs_config_load_startup()` in `Shock.c`'s `main()`, right after
|
||||||
|
`LoadPrefs()`, and one check in `sdl_events.c`'s `pump_events()` to drain
|
||||||
|
the custom SDL events the menu's Save/Load Configuration buttons enqueue.
|
||||||
|
|
||||||
`android/gl4es-patches/`:
|
`android/gl4es-patches/`:
|
||||||
|
|
||||||
@@ -517,10 +616,241 @@ left behind elsewhere in the frame and misreads every row - a diagonal
|
|||||||
shear/banding pattern. Menus never trigger it because nothing in the
|
shear/banding pattern. Menus never trigger it because nothing in the
|
||||||
menu-only code paths sets a custom unpack stride first.
|
menu-only code paths sets a custom unpack stride first.
|
||||||
|
|
||||||
**Fix** (`14-android-quad-unpack-state.patch`): explicitly reset both
|
#### 5.5.3. In-game SFX and MIDI music didn't play
|
||||||
`GL_UNPACK_ROW_LENGTH` and `GL_UNPACK_ALIGNMENT` to their tightly-packed
|
|
||||||
values immediately before this `glTexImage2D` call, removing the
|
Intro cutscene audio played correctly, but in-game SFX and MIDI/background
|
||||||
dependency on whatever GL state preceded it.
|
music never played, on-device.
|
||||||
|
|
||||||
|
**Root cause:** `snd_start_digital()` in `engine/src/MacSrc/SDLSound.c`
|
||||||
|
opened two separate SDL audio devices - a raw `SDL_OpenAudioDevice()`
|
||||||
|
call (used for cutscene/movie audio, always succeeded) and a second
|
||||||
|
device via `Mix_OpenAudio()` (used for SFX and, via `Mix_HookMusic`,
|
||||||
|
MIDI music - always failed, `SDL_GetError()` reporting "Audio device
|
||||||
|
already open"). SDL's Android audio backends (both AAudio and OpenSL ES)
|
||||||
|
only ever allow one open non-capture device - forcing OpenSL ES instead
|
||||||
|
of AAudio (`05-android-audio-driver.patch`) doesn't change this, since
|
||||||
|
the restriction isn't backend-specific.
|
||||||
|
|
||||||
|
**Fix (`16-android-audio-single-device.patch`):** removed the raw
|
||||||
|
`SDL_OpenAudioDevice()` call entirely. Cutscene/audiolog PCM (previously
|
||||||
|
delivered via that device's own callback) is now mixed into the single
|
||||||
|
`Mix_OpenAudio()` device's output via `Mix_SetPostMix()`, additively
|
||||||
|
(`SDL_MixAudioFormat`) rather than overwritten, so SFX/MIDI music
|
||||||
|
already mixed into the buffer isn't silenced whenever a cutscene or
|
||||||
|
audiolog plays. The stream-pointer swaps in `cutsloop.c`/`audiolog.c`
|
||||||
|
that used to be guarded by pausing the now-removed device are guarded by
|
||||||
|
`SDL_LockAudio()`/`SDL_UnlockAudio()` instead, since the post-mix hook
|
||||||
|
runs continuously on SDL_mixer's own audio thread.
|
||||||
|
|
||||||
|
#### 5.5.4. MIDI music silent even after audio otherwise worked
|
||||||
|
|
||||||
|
After fixing 5.5.3, SFX and cutscene audio played correctly, but MIDI
|
||||||
|
background music was still silent, with logcat showing `Creating native
|
||||||
|
MIDI device` instead of `Creating FluidSynth MIDI device`.
|
||||||
|
|
||||||
|
**Root cause:** the engine's `midi-backend` preference (`0` = ADLMIDI,
|
||||||
|
`1` = Native, `2` = FluidSynth) is persisted in a `prefs.txt` file in the
|
||||||
|
app's private storage, which Android retains across every APK
|
||||||
|
reinstall/upgrade (only a full uninstall clears it). A stale value of
|
||||||
|
`1` (Native) from earlier testing was carried forward through every
|
||||||
|
rebuild this whole session. `SetDefaultPrefs()` (`Prefs.c`) does default
|
||||||
|
to `2` (FluidSynth) whenever `USE_FLUIDSYNTH` is defined, which it
|
||||||
|
always has been for this build - the default was never wrong, only the
|
||||||
|
persisted override. `NativeMidiInit()` has no real implementation on
|
||||||
|
Android (see `17-android-native-midi-unsupported.patch`), so it silently
|
||||||
|
reported success without opening anything.
|
||||||
|
|
||||||
|
**Fix:** a full uninstall (not just installing a new build over the old
|
||||||
|
one) clears the stale preference, after which MIDI plays correctly.
|
||||||
|
`17-android-native-midi-unsupported.patch` additionally makes
|
||||||
|
`NativeMidiInit()` report failure on unsupported platforms, so a stale
|
||||||
|
"Native" preference falls back to `InitDecXMI()`'s existing dummy-driver
|
||||||
|
path (still silent, but with a clear `WARN` in logcat) instead of
|
||||||
|
silently reporting success while never opening anything - a much faster
|
||||||
|
diagnosis if this ever recurs.
|
||||||
|
|
||||||
|
#### 5.5.5. Black, then flat-colored/untextured screen after switching to OpenGL rendering in-game
|
||||||
|
|
||||||
|
The engine has a `Ctrl+G`-bound in-game toggle (`toggle_opengl()`,
|
||||||
|
`Prefs.c`) that cycles between the plain software renderer and OpenGL
|
||||||
|
rendering. On Quest, switching to OpenGL rendering (either mode:
|
||||||
|
unfiltered or bilinear) produced a black screen for the 3D view, with
|
||||||
|
`OpenGL error: 1282` (`GL_INVALID_OPERATION`) in logcat right at the switch.
|
||||||
|
|
||||||
|
**Root cause:** `opengl_swap_and_restore()` draws the offscreen-rendered
|
||||||
|
3D view onto a screen-filling quad using `glBegin(GL_TRIANGLE_STRIP)` /
|
||||||
|
`glVertexAttrib2f()`/`glVertexAttrib1f()` / `glVertex3f()` immediate
|
||||||
|
mode to supply per-vertex texcoords and light. This is exactly the same
|
||||||
|
bug already found and fixed for the UI-overlay quad in
|
||||||
|
`11-android-openxr-present.patch`'s `android_draw_surface_as_quad()`:
|
||||||
|
under gl4es's GLES2 immediate-mode emulation, `glVertexAttrib*()` sets a
|
||||||
|
*constant* value for the whole draw call, not a fresh value per vertex
|
||||||
|
the way desktop GL's immediate mode does - only attribute 0 (position,
|
||||||
|
driven by `glVertex3f`) gets real per-vertex capture. The 3D-view quad
|
||||||
|
draw predates that fix and was never updated to match, so it kept using
|
||||||
|
the broken pattern.
|
||||||
|
|
||||||
|
This patch's first revision only fixed that one quad, which did clear the
|
||||||
|
black screen - but the next on-device test showed a "blocky" world
|
||||||
|
instead: every wall/floor polygon rendered as a single solid color
|
||||||
|
instead of its texture. The same broken immediate-mode pattern turned out
|
||||||
|
to be used by every other textured/lit polygon draw in the file too:
|
||||||
|
`opengl_light_tmap()` (every textured wall/floor polygon in the 3D view,
|
||||||
|
via the shared `draw_vertex()` helper), `opengl_bitmap()` (sprites), and
|
||||||
|
`opengl_draw_poly()` (flat/gouraud-shaded polygons, also via
|
||||||
|
`draw_vertex()`) - each polygon's texcoord/light attribute ended up
|
||||||
|
constant across all of its vertices (whatever was last set), instead of
|
||||||
|
varying per vertex, hence the flat, solid-colored blocks.
|
||||||
|
|
||||||
|
**Fix:** `18-android-opengl-immediate-mode-attribs.patch` adds
|
||||||
|
`android_imm_begin()`/`android_imm_vertex()`/`android_imm_end()`, a small
|
||||||
|
shared immediate-mode-emulation shim (buffers vertices, then issues one
|
||||||
|
real `glDrawArrays()` call), and switches every glBegin/glVertexAttrib/
|
||||||
|
glVertex3f call site above to it, Android-only - the desktop path is
|
||||||
|
unaffected and keeps the original immediate-mode code, which works
|
||||||
|
correctly under real desktop OpenGL.
|
||||||
|
|
||||||
|
The same patch also fixes the starfield
|
||||||
|
(`opengl_begin_stars()`/`opengl_draw_star()`/`opengl_end_stars()`), found
|
||||||
|
while auditing the file for every other instance of this pattern rather
|
||||||
|
than through an on-device symptom: each star is a separate `GL_POINTS`
|
||||||
|
vertex with its own brightness set via the same
|
||||||
|
`glVertexAttrib1f`/`glVertex3f` immediate-mode pair, so every star drawn
|
||||||
|
between one begin/end call would have rendered at whatever brightness was
|
||||||
|
set for the last one. Gets its own `android_stars_begin()`/
|
||||||
|
`android_stars_vertex()`/`android_stars_end()` buffer, shaped for
|
||||||
|
`GL_POINTS` and up to `NUM_STARS` (`GameSrc/rendtool.c`) vertices rather
|
||||||
|
than `android_imm_*()`'s small triangle-strip buffer.
|
||||||
|
|
||||||
|
#### 5.5.6. Display breaks and stops responding to input after changing resolution in-game
|
||||||
|
|
||||||
|
Picking a different resolution from the in-game options menu (e.g.
|
||||||
|
`1024x768`) produced a single `OpenGL error: 1282` in logcat and left the
|
||||||
|
display frozen on the last frame - `VrApi`'s FPS counter kept running at
|
||||||
|
90fps (the OpenXR compositor was still submitting frames), but nothing
|
||||||
|
in the menu responded to further input afterward.
|
||||||
|
|
||||||
|
**Root cause:** `ChangeScreenSize()` (`ShockBitmap.c`) calls
|
||||||
|
`SDL_RenderSetLogicalSize()` to the new resolution and reallocates
|
||||||
|
`drawSurface`/`offscreenDrawSurface` accordingly, but on desktop the
|
||||||
|
actual GL-side work - `opengl_resize()` in `OpenGL.cc`, which
|
||||||
|
(re)creates `backupBuffer` (the offscreen render target the 3D view
|
||||||
|
draws into) at the new size - only runs later, from the
|
||||||
|
`SDL_WINDOWEVENT_RESIZED`/`SIZE_CHANGED` event that
|
||||||
|
`SDL_SetWindowSize()` generates. `09-android-no-window-resize.patch`
|
||||||
|
skips that `SDL_SetWindowSize()` call on Android for an unrelated
|
||||||
|
reason (see its own entry above) - which also means no such event, and
|
||||||
|
therefore no `opengl_resize()` call, ever happens on Android when the
|
||||||
|
in-game resolution changes. `backupBuffer` stayed stuck at whatever size
|
||||||
|
it was created with at launch while the rest of the game moved on to
|
||||||
|
the new resolution, and that mismatch is what broke rendering and left
|
||||||
|
the menu unable to correctly hit-test further clicks.
|
||||||
|
|
||||||
|
**Fix:** `ChangeScreenSize()` now calls `opengl_resize()` directly on
|
||||||
|
Android, right after updating `gScreenWide`/`gScreenHigh` (matching the
|
||||||
|
desktop event's actual timing, since `SDL_PollEvent()` always delivers
|
||||||
|
it after `ChangeScreenSize()` has already returned) - added to
|
||||||
|
`09-android-no-window-resize.patch` since this is a direct, foreseeable
|
||||||
|
consequence of that patch's own change, not a new mechanism.
|
||||||
|
|
||||||
|
#### 5.5.7. Laser-pointer cursor disappears after changing resolution in-game
|
||||||
|
|
||||||
|
After 5.5.6's fix, changing resolution stopped breaking the display, but
|
||||||
|
the laser-pointer-driven mouse cursor (see section 5's architecture
|
||||||
|
overview) would disappear and stop being able to click anything
|
||||||
|
afterward.
|
||||||
|
|
||||||
|
**Root cause:** `xr_get_game_resolution()` (`xr_session.c`), which
|
||||||
|
`xr_input.c` uses to convert a game-quad ray hit's normalized u,v into
|
||||||
|
the pixel coordinates it feeds the engine via `xr_mouse_move()`, returned
|
||||||
|
the game quad's OpenXR swapchain's fixed pixel size
|
||||||
|
(`g_game_swapchain.width/height`) - set once at launch and never resized.
|
||||||
|
`android_draw_surface_as_quad()` (`OpenGL.cc`,
|
||||||
|
`11-android-openxr-present.patch`) always stretch-blits `drawSurface` to
|
||||||
|
fill that swapchain image via a single full-quad draw, regardless of
|
||||||
|
`drawSurface`'s own pixel size, so the swapchain's fixed size has no
|
||||||
|
actual relationship to the coordinate space `MouseX`/`MouseY` live in -
|
||||||
|
that's `drawSurface`'s own current size (`gScreenWide`/`gScreenHigh`),
|
||||||
|
which `ChangeScreenSize()` can change at runtime independently of the
|
||||||
|
swapchain. Using the stale swapchain size there mapped every ray hit
|
||||||
|
into the wrong coordinate space, moving the mouse position out of the
|
||||||
|
engine's actual screen bounds.
|
||||||
|
|
||||||
|
**Fix:** `xr_get_game_resolution()` now returns `gScreenWide`/
|
||||||
|
`gScreenHigh` instead. `xr_get_game_quad_extent()` (the quad's shape and
|
||||||
|
distance in the VR world) is intentionally left as-is, still tied to the
|
||||||
|
swapchain's fixed size - that's a real, unchanging property of the
|
||||||
|
swapchain image itself, unrelated to this bug.
|
||||||
|
|
||||||
|
#### 5.5.8. Persistent `OpenGL error: 1282` logcat spam during 3D gameplay
|
||||||
|
|
||||||
|
Every logcat capture during actual 3D gameplay showed a continuous
|
||||||
|
stream of `OpenGL error: 1282` (roughly twice per frame) from the one
|
||||||
|
pre-existing `glGetError()` check in `opengl_swap_and_restore()`, with
|
||||||
|
no visible effect on rendering (textures, resolution changes, and the
|
||||||
|
cursor all worked correctly per sections 5.5.5-5.5.7's fixes). 1282 is
|
||||||
|
`GL_INVALID_OPERATION` (0x0502) - not `GL_INVALID_ENUM` (0x0500/1280),
|
||||||
|
which earlier debugging had mistakenly assumed given the visual
|
||||||
|
symptoms resembled the immediate-mode bug in 5.5.5.
|
||||||
|
|
||||||
|
Two real, unrelated bugs turned out to be involved, found by adding a
|
||||||
|
temporary chain of `glGetError()` checkpoints across the whole
|
||||||
|
per-frame render path (`mainloop()` → `render_run()` → `fr_rend()` →
|
||||||
|
`fr_prepare_view()`/`fr_start_view()`/`fr_send_view()`) and bisecting
|
||||||
|
which ones read clean:
|
||||||
|
|
||||||
|
**xrPollEvent() leaves a stray error.** `xr_poll_events()`
|
||||||
|
(`xr_session.c`) left gl4es reporting `GL_INVALID_OPERATION` on every
|
||||||
|
call while the session is running, regardless of what this app's own
|
||||||
|
code does. Fixed by draining it (`while (glGetError() != GL_NO_ERROR)`)
|
||||||
|
right after the polling loop.
|
||||||
|
|
||||||
|
**A hack-camera render re-entered the OpenXR frame setup.**
|
||||||
|
`render_hack_cameras()` (`render.c`) calls `fr_rend()` - and so
|
||||||
|
`fr_start_view()`/`fr_send_view()` - a second time per real frame for
|
||||||
|
each active security-camera monitor, rendering into a private software
|
||||||
|
(`FR_DOUBLEB_MASK`) bitmap rather than the actual GL-backed screen.
|
||||||
|
`use_opengl()` already excludes hack-camera/pickup-mode views from the
|
||||||
|
GL texture-mapping functions (`FR_PICKUPM_MASK|FR_HACKCAM_MASK`), but
|
||||||
|
`should_opengl_swap()` - the separate check gating
|
||||||
|
`opengl_start_frame()`/`opengl_end_frame()` in those two functions -
|
||||||
|
didn't, so those ran again mid-frame for a render that was never
|
||||||
|
GL-backed. `19-android-fix-hackcam-opengl-frame.patch` adds the same
|
||||||
|
exclusion to both call sites. This is a genuine bug fixed on its own
|
||||||
|
merits, but bisecting further showed it wasn't actually the source of
|
||||||
|
the 1282 spam - removing it just relocated the checkpoint that caught
|
||||||
|
the error, without changing whether it fired.
|
||||||
|
|
||||||
|
**Actual root cause: binding "framebuffer 0" on Android has no valid
|
||||||
|
target.** `opengl_end_frame()`'s `BindFrameBuffer(nullptr)` binds
|
||||||
|
framebuffer/renderbuffer id 0 to mean "the real window surface" - true
|
||||||
|
on desktop, where that's SDL's actual GL window. On Android, this app's
|
||||||
|
real presentation target is never that window's own EGL surface at
|
||||||
|
all - it's the OpenXR swapchain's per-image FBOs
|
||||||
|
(`xr_swapchain_create()`/`xr_swapchain_acquire()`, `xr_swapchain.c`).
|
||||||
|
Whatever framebuffer/renderbuffer 0 actually resolves to for gl4es in
|
||||||
|
this configuration, binding it reliably set a persistent
|
||||||
|
`GL_INVALID_OPERATION` - confirmed by bisecting through the *entire*
|
||||||
|
render path with per-call checkpoints, all clean right up until the one
|
||||||
|
placed immediately after this exact call. The bind is also functionally
|
||||||
|
dead regardless of the error: by the time anything next draws, Shock.c's
|
||||||
|
`SDLDraw()` has already run `xr_frame_begin()` →
|
||||||
|
`xr_swapchain_acquire()`, which rebinds to that frame's real swapchain
|
||||||
|
FBO before `opengl_swap_and_restore()` (the only thing that draws before
|
||||||
|
the next `opengl_start_frame()`'s own `BindFrameBuffer(&backupBuffer)`)
|
||||||
|
runs - so nothing ever actually renders while framebuffer 0 is bound.
|
||||||
|
`20-android-no-default-framebuffer.patch` skips the call on Android
|
||||||
|
entirely. Confirmed via logcat: this fully eliminated the error across
|
||||||
|
an extended play session with 3D rendering active throughout, with no
|
||||||
|
change in what actually renders.
|
||||||
|
|
||||||
|
An earlier theory (that the OpenXR runtime's own GL work inside
|
||||||
|
`xrAcquireSwapchainImage`/`xrWaitSwapchainImage`/`xrEndFrame` was
|
||||||
|
leaving stray driver-level errors on the shared EGL context) prompted a
|
||||||
|
dlsym'd, gl4es-bypassing `glGetError()` drain at those call sites. Once
|
||||||
|
the above was fixed, testing showed that drain was never actually
|
||||||
|
necessary, so it was removed rather than kept as unproven defensive
|
||||||
|
code.
|
||||||
|
|
||||||
## 6. License
|
## 6. License
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,33 @@ if (prebuiltDir == null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set by build-apk.sh (see build-image/version.sh, shared with the
|
// Set by build-apk.sh (see build-image/version.sh, shared with the
|
||||||
// desktop tarball's versioning) via -PquestshockVersionName/Code. Default
|
// desktop tarball's versioning) via -PquestshockVersionName/Code. Building
|
||||||
// here only covers a direct, unsupported `gradlew` invocation.
|
// directly from Android Studio (the normal day-to-day workflow - see
|
||||||
def questshockVersionName = project.hasProperty('questshockVersionName') ? project.property('questshockVersionName') : '0.0.0-dev'
|
// README) never passes that property, so the default below has to carry
|
||||||
|
// its own weight rather than just being a fallback for some rare, other,
|
||||||
|
// unsupported path: it runs `git describe` itself, the same way
|
||||||
|
// engine/CMakeLists.txt's own PROJECT_REVERSION_STRING does for the
|
||||||
|
// engine's *own* separate version string - "vX.Y.Z" on a clean tagged
|
||||||
|
// commit, or "vX.Y.Z-N-gHASH[-dirty]" otherwise, so a build between
|
||||||
|
// releases is still identifiable from the in-headset menu (see
|
||||||
|
// MenuOverlay.java's header, which reads BuildConfig.VERSION_NAME).
|
||||||
|
def gitDescribeVersionName = {
|
||||||
|
try {
|
||||||
|
def proc = ["git", "-C", projectDir.toString(), "describe", "--tags", "--always", "--dirty"].execute()
|
||||||
|
proc.waitFor()
|
||||||
|
if (proc.exitValue() == 0) {
|
||||||
|
def out = proc.text.trim()
|
||||||
|
if (out) {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// git not on PATH, or this isn't a git checkout at all (e.g. a
|
||||||
|
// source tarball) - fall through to the placeholder below.
|
||||||
|
}
|
||||||
|
return "0.0.0-dev"
|
||||||
|
}
|
||||||
|
def questshockVersionName = project.hasProperty('questshockVersionName') ? project.property('questshockVersionName') : gitDescribeVersionName()
|
||||||
def questshockVersionCode = project.hasProperty('questshockVersionCode') ? project.property('questshockVersionCode').toInteger() : 1
|
def questshockVersionCode = project.hasProperty('questshockVersionCode') ? project.property('questshockVersionCode').toInteger() : 1
|
||||||
|
|
||||||
// Auto-refreshes the staged, patched engine/ and gl4es-src/ copies and
|
// Auto-refreshes the staged, patched engine/ and gl4es-src/ copies and
|
||||||
@@ -80,10 +104,14 @@ def stageEngineTask = tasks.register("stageEngine", Exec) {
|
|||||||
inputs.dir("${projectDir}/../gl4es-src")
|
inputs.dir("${projectDir}/../gl4es-src")
|
||||||
inputs.dir("${projectDir}/../gl4es-patches")
|
inputs.dir("${projectDir}/../gl4es-patches")
|
||||||
inputs.file("${repoRoot}/build-image/Dockerfile")
|
inputs.file("${repoRoot}/build-image/Dockerfile")
|
||||||
|
// Staged into assets/ by prepare-android-project.sh.
|
||||||
|
inputs.file("${repoRoot}/res/questshock.config.default.json")
|
||||||
|
inputs.dir("${repoRoot}/res/controller_profiles")
|
||||||
outputs.dir("${repoRoot}/build/android-engine")
|
outputs.dir("${repoRoot}/build/android-engine")
|
||||||
outputs.dir("${repoRoot}/build/android-gl4es")
|
outputs.dir("${repoRoot}/build/android-gl4es")
|
||||||
outputs.dir("${repoRoot}/build/android-prebuilt")
|
outputs.dir("${repoRoot}/build/android-prebuilt")
|
||||||
outputs.file("${projectDir}/../engine.properties")
|
outputs.file("${projectDir}/../engine.properties")
|
||||||
|
outputs.dir("${projectDir}/src/main/assets")
|
||||||
}
|
}
|
||||||
|
|
||||||
// preBuild is what every variant's compile/native-build tasks already
|
// preBuild is what every variant's compile/native-build tasks already
|
||||||
@@ -171,7 +199,7 @@ android {
|
|||||||
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \
|
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \
|
||||||
"-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \
|
"-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \
|
||||||
"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \
|
"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \
|
||||||
"-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c;${projectDir}/src/main/cpp/xr_mouse.c"
|
"-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c;${projectDir}/src/main/cpp/xr_mouse.c;${projectDir}/src/main/cpp/qs_config.c"
|
||||||
abiFilters 'arm64-v8a'
|
abiFilters 'arm64-v8a'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +217,13 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generates BuildConfig (specifically BuildConfig.VERSION_NAME, read by
|
||||||
|
// MenuOverlay.java's header) - off by default since AGP 8.0, so this
|
||||||
|
// has to be requested explicitly.
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#ifndef QUESTSHOCK_QS_CONFIG_H
|
||||||
|
#define QUESTSHOCK_QS_CONFIG_H
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Persistent, human-editable JSON config file - see CONFIG.md at the repo
|
||||||
|
// root for the documented schema. Lives at a plain relative path
|
||||||
|
// ("questshock.config.json") since the whole engine already runs with its
|
||||||
|
// cwd chdir()'d to the user-facing /sdcard/questshock/ folder (see
|
||||||
|
// QuestShockActivity.java's nativeChdir()) before main() ever runs - the
|
||||||
|
// same way prefs.txt/keybinds.txt (Prefs.c) already do.
|
||||||
|
//
|
||||||
|
// Deliberately independent of prefs.txt: this file is never read or written
|
||||||
|
// as a side effect of prefs.txt load/save, and vice versa.
|
||||||
|
|
||||||
|
// Parses the config file if it exists (silent no-op if it doesn't - this
|
||||||
|
// never creates the file itself) and applies every recognized option
|
||||||
|
// directly into gShockPrefs/mode_id. Call once, early in main() - see
|
||||||
|
// android/engine-patches/21-android-config.patch - after LoadPrefs() (so the
|
||||||
|
// config file can override prefs.txt) and before init_all() (so there's no
|
||||||
|
// live-vs-restart distinction to worry about: nothing is initialized yet, so
|
||||||
|
// every field takes full effect as part of this same startup).
|
||||||
|
void qs_config_load_startup(void);
|
||||||
|
|
||||||
|
// Called from the "Save Configuration"/"Load Configuration" button JNI
|
||||||
|
// handlers (questshock_native.c), which run on Android's UI thread. These
|
||||||
|
// only enqueue a custom SDL event - the actual file I/O and any
|
||||||
|
// gShockPrefs/mode_id/message_info() work happens on the engine's own SDL
|
||||||
|
// main thread instead (see qs_config_handle_event() below), matching this
|
||||||
|
// codebase's existing convention for crossing this exact thread boundary
|
||||||
|
// (xr_session.c's xr_poll_events(), questshock_native.c's SDL_TEXTINPUT
|
||||||
|
// push for keyboard input).
|
||||||
|
void qs_config_request_save(void);
|
||||||
|
void qs_config_request_load(void);
|
||||||
|
|
||||||
|
// Same cross-thread-dispatch reasoning as qs_config_request_save()/_load()
|
||||||
|
// above - called from the Video Options tab's controls (MenuOverlay.java),
|
||||||
|
// which are just as much on Android's UI thread as the Save/Load buttons.
|
||||||
|
// opengl=true/false selects "opengl"/"software"; resolution_index is 0-4,
|
||||||
|
// indexing the same table qs_config_current_resolution_index() below
|
||||||
|
// reads. Applies live immediately (no restart involved - see CONFIG.md);
|
||||||
|
// unlike Load Configuration, these don't post a message_info() HUD
|
||||||
|
// notification for every single tap - the tab's own selection highlight is
|
||||||
|
// the feedback.
|
||||||
|
void qs_config_request_set_renderer(bool opengl);
|
||||||
|
void qs_config_request_set_resolution(int resolution_index);
|
||||||
|
|
||||||
|
// Same cross-thread-dispatch reasoning as the two above - called from the
|
||||||
|
// Main Menu tab's Language row (MenuOverlay.java). language_index is 0
|
||||||
|
// English/1 French/2 German (gShockPrefs.goLanguage's own numbering,
|
||||||
|
// Prefs.h). Applies live immediately, the same way the game's own options
|
||||||
|
// menu applies a language change (see qs_config.c's qs_apply_language()).
|
||||||
|
void qs_config_request_set_language(int language_index);
|
||||||
|
|
||||||
|
// Same cross-thread dispatch as the three above - called from the Main Menu
|
||||||
|
// tab's "Reset Position" button. Re-anchors the player's position in front of
|
||||||
|
// the game quad (see xr_input_reset_player_position()); never reads or writes
|
||||||
|
// the config file.
|
||||||
|
void qs_config_request_reset_position(void);
|
||||||
|
|
||||||
|
// Same cross-thread-dispatch reasoning as the four above - called from the
|
||||||
|
// Video Options tab's "Initial Screen"/"Game Screen" width/distance
|
||||||
|
// preset rows (MenuOverlay.java). preset_index indexes qs_config.c's own
|
||||||
|
// shared, offset+step-generated width/distance preset lists (both in
|
||||||
|
// meters; the offset/step themselves are config-file-only, not exposed
|
||||||
|
// as buttons), read back via the four qs_config_current_*_screen_*_index()
|
||||||
|
// getters below. Applies live immediately if the just-changed profile is
|
||||||
|
// the currently active one (see CONFIG.md) - otherwise just updates the
|
||||||
|
// stored preference, invisibly, until that profile becomes active (e.g.
|
||||||
|
// tweaking "Initial Screen" mid-game).
|
||||||
|
void qs_config_request_set_initial_screen_width(int preset_index);
|
||||||
|
void qs_config_request_set_initial_screen_distance(int preset_index);
|
||||||
|
void qs_config_request_set_game_screen_width(int preset_index);
|
||||||
|
void qs_config_request_set_game_screen_distance(int preset_index);
|
||||||
|
|
||||||
|
// Cross-thread requests from the Controller Options tab (MenuOverlay.java),
|
||||||
|
// enqueued like every request_*() above. action_index indexes
|
||||||
|
// xr_input_action_name()'s list. Begin/cancel start and stop rebind capture;
|
||||||
|
// reset restores all bindings (the built-in defaults, then
|
||||||
|
// questshock.config.default.json's player_controls group if it has one).
|
||||||
|
void qs_config_request_begin_control_capture(int action_index);
|
||||||
|
void qs_config_request_cancel_control_capture(void);
|
||||||
|
void qs_config_request_reset_player_controls(void);
|
||||||
|
|
||||||
|
// Called from xr_input.c's xr_input_process_capture() on the engine's own
|
||||||
|
// thread, so no cross-thread dispatch is needed (as with
|
||||||
|
// qs_screen_profile_notify_loop_mode() below). Resolves
|
||||||
|
// MenuOverlay.onControlCaptureResult(int, String) for action_index (raw_name
|
||||||
|
// NULL on timeout or cancel), and again for displaced_action_index if the new
|
||||||
|
// binding took the input from another action (-1/NULL when none), so both
|
||||||
|
// rows can be updated together.
|
||||||
|
void qs_config_notify_control_capture_result(int action_index, const char *raw_name,
|
||||||
|
int displaced_action_index,
|
||||||
|
const char *displaced_raw_name);
|
||||||
|
|
||||||
|
// Called from xr_input.c whenever the VR menu's visibility changes. Showing
|
||||||
|
// the menu pauses a running game (not the main menu, intro or cutscenes) that
|
||||||
|
// is not already paused; hiding it unpauses only if the menu did the pausing.
|
||||||
|
// Enqueued like the request_*() functions above; the pause itself runs on the
|
||||||
|
// engine's thread at pump_events() time.
|
||||||
|
void qs_config_notify_menu_visibility(bool visible);
|
||||||
|
|
||||||
|
// Called from xr_input.c's xr_input_suggest_bindings() when the OpenXR runtime
|
||||||
|
// rejects a supported interaction profile's suggested bindings at startup.
|
||||||
|
// That is an app bug, not a config problem, so it shows
|
||||||
|
// MenuOverlay.showFatalControllerBindingError()'s non-cancelable "Exit" dialog;
|
||||||
|
// xr_input_init() aborts right after.
|
||||||
|
void qs_config_report_fatal_input_error(const char *profile_display_name);
|
||||||
|
|
||||||
|
// Returns true and handles the event if it is one of the custom events the
|
||||||
|
// request_*()/notify functions above enqueue. Called from pump_events()
|
||||||
|
// (sdl_events.c, see android/engine-patches/21-android-config.patch) on the
|
||||||
|
// engine's thread, once per polled event. Returns false for any other event
|
||||||
|
// type, including before the custom types are registered.
|
||||||
|
bool qs_config_handle_event(const SDL_Event *event);
|
||||||
|
|
||||||
|
// Called directly (no event-queue dispatch - see
|
||||||
|
// android/engine-patches/22-android-screen-profile.patch and this
|
||||||
|
// function's own definition in qs_config.c for why) from the engine's
|
||||||
|
// mainloop.c whenever the loop mode changes, and once from
|
||||||
|
// qs_config_load_startup() - picks between the "Initial Screen" (menu/
|
||||||
|
// intro) and "Game Screen" (actual gameplay) presets based on new_mode
|
||||||
|
// (mainloop.h's SETUP_LOOP/CUTSCENE_LOOP vs. everything else) and applies
|
||||||
|
// the newly-active one's shape to xr_session.c, but only if it's actually
|
||||||
|
// different from what was already active.
|
||||||
|
void qs_screen_profile_notify_loop_mode(short new_mode);
|
||||||
|
|
||||||
|
// Read-only queries for the Video Options tab's own selection highlight
|
||||||
|
// (MenuOverlay.java re-reads these whenever the tab is (re)shown, so it
|
||||||
|
// reflects the live value even if it was last changed some other way, e.g.
|
||||||
|
// the Ctrl+G hotkey or Load Configuration). Safe to call directly from
|
||||||
|
// Android's UI thread despite not going through the event-queue dispatch
|
||||||
|
// the mutating calls above use - same low-risk reasoning already used for
|
||||||
|
// Save Configuration's own reads of gShockPrefs/mode_id: these are simple,
|
||||||
|
// independent POD scalar reads, not a sequence that needs to be atomic
|
||||||
|
// with anything else.
|
||||||
|
bool qs_config_current_renderer_is_opengl(void);
|
||||||
|
int qs_config_current_resolution_index(void);
|
||||||
|
|
||||||
|
// -1 (not just an unlikely fallback, a routine, expected result here -
|
||||||
|
// unlike qs_config_current_resolution_index() above) if the profile's
|
||||||
|
// current value doesn't exactly match one of qs_config.c's own
|
||||||
|
// offset+step-generated width/distance presets, e.g. after a hand-edited
|
||||||
|
// config file sets an in-between value - MenuOverlay.java's own
|
||||||
|
// setSelected() naturally shows no button selected in that case, no
|
||||||
|
// special-casing needed.
|
||||||
|
int qs_config_current_initial_screen_width_index(void);
|
||||||
|
int qs_config_current_initial_screen_distance_index(void);
|
||||||
|
int qs_config_current_game_screen_width_index(void);
|
||||||
|
int qs_config_current_game_screen_distance_index(void);
|
||||||
|
|
||||||
|
// Returns the value preset button `index` (0..4) currently shows for the
|
||||||
|
// width/distance option - shared by both "Initial Screen" and "Game
|
||||||
|
// Screen" columns (their offset/step is config-file-only, not per-profile
|
||||||
|
// - see qs_config.c's own comment on the underlying statics), so
|
||||||
|
// MenuOverlay.java can label its preset buttons correctly even after a
|
||||||
|
// Load Configuration changes the offset/step. Index is clamped into
|
||||||
|
// range, not validated - every index 0..4 always has a value.
|
||||||
|
float qs_config_screen_width_preset_value(int index);
|
||||||
|
float qs_config_screen_distance_preset_value(int index);
|
||||||
|
|
||||||
|
// gShockPrefs.goLanguage (0 English/1 French/2 German, Prefs.h) - read by
|
||||||
|
// MenuOverlay.java's/KeyboardOverlay.java's periodic header-refresh timer
|
||||||
|
// to pick which language's chrome text (Strings.java) to display, so a
|
||||||
|
// language change made mid-session via the game's own options menu (or our
|
||||||
|
// own Language row, or Load Configuration) is picked up without needing to
|
||||||
|
// reopen either panel; also doubles as the Main Menu tab's Language row's
|
||||||
|
// own selection highlight, same as qs_config_current_renderer_is_opengl()/
|
||||||
|
// qs_config_current_resolution_index() above. Same off-thread-scalar-read
|
||||||
|
// reasoning as those two getters.
|
||||||
|
int qs_config_current_language(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // QUESTSHOCK_QS_CONFIG_H
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include "qs_config.h"
|
||||||
|
#include "xr_input.h"
|
||||||
#include "xr_overlay.h"
|
#include "xr_overlay.h"
|
||||||
#include "xr_session.h"
|
#include "xr_session.h"
|
||||||
|
|
||||||
@@ -88,3 +90,183 @@ JNIEXPORT void JNICALL
|
|||||||
Java_de_ladkau_questshock_MenuOverlay_nativeShowKeyboard(JNIEnv *env, jclass clazz) {
|
Java_de_ladkau_questshock_MenuOverlay_nativeShowKeyboard(JNIEnv *env, jclass clazz) {
|
||||||
xr_overlay_set_visible(xr_session_get_keyboard_overlay(), true);
|
xr_overlay_set_visible(xr_session_get_keyboard_overlay(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MenuOverlay's "Save Configuration"/"Load Configuration" buttons - see
|
||||||
|
// qs_config.h for why these only enqueue an event rather than doing the
|
||||||
|
// actual file/engine-state work here directly (this runs on Android's UI
|
||||||
|
// thread, not the engine's own).
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSaveConfig(JNIEnv *env, jclass clazz) {
|
||||||
|
qs_config_request_save();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestLoadConfig(JNIEnv *env, jclass clazz) {
|
||||||
|
qs_config_request_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuOverlay's Video Options tab - live-appliable controls, see
|
||||||
|
// qs_config.h. Same enqueue-only reasoning as the Save/Load buttons above.
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetRenderer(JNIEnv *env, jclass clazz,
|
||||||
|
jboolean opengl) {
|
||||||
|
qs_config_request_set_renderer(opengl);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetResolution(JNIEnv *env, jclass clazz,
|
||||||
|
jint resolutionIndex) {
|
||||||
|
qs_config_request_set_resolution(resolutionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuOverlay's Main Menu tab's Language row - same enqueue-only reasoning as above.
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetLanguage(JNIEnv *env, jclass clazz,
|
||||||
|
jint 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuOverlay's Video Options tab - "Initial Screen"/"Game Screen" width/
|
||||||
|
// distance preset rows, same enqueue-only reasoning as above.
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetInitialScreenWidth(JNIEnv *env, jclass clazz,
|
||||||
|
jint presetIndex) {
|
||||||
|
qs_config_request_set_initial_screen_width(presetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetGameScreenWidth(JNIEnv *env, jclass clazz,
|
||||||
|
jint presetIndex) {
|
||||||
|
qs_config_request_set_game_screen_width(presetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetInitialScreenDistance(JNIEnv *env,
|
||||||
|
jclass clazz,
|
||||||
|
jint presetIndex) {
|
||||||
|
qs_config_request_set_initial_screen_distance(presetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetGameScreenDistance(JNIEnv *env, jclass clazz,
|
||||||
|
jint presetIndex) {
|
||||||
|
qs_config_request_set_game_screen_distance(presetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read-only queries - safe to call directly (no enqueue needed, see
|
||||||
|
// qs_config.h's own comment on each of these).
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetRendererIsOpenGl(JNIEnv *env, jclass clazz) {
|
||||||
|
return qs_config_current_renderer_is_opengl();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetResolutionIndex(JNIEnv *env, jclass clazz) {
|
||||||
|
return qs_config_current_resolution_index();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetInitialScreenWidthIndex(JNIEnv *env, jclass clazz) {
|
||||||
|
return qs_config_current_initial_screen_width_index();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetGameScreenWidthIndex(JNIEnv *env, jclass clazz) {
|
||||||
|
return qs_config_current_game_screen_width_index();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetInitialScreenDistanceIndex(JNIEnv *env,
|
||||||
|
jclass clazz) {
|
||||||
|
return qs_config_current_initial_screen_distance_index();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetGameScreenDistanceIndex(JNIEnv *env, jclass clazz) {
|
||||||
|
return qs_config_current_game_screen_distance_index();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preset button *labels* (not the current value) - shared by both
|
||||||
|
// "Initial Screen" and "Game Screen" columns, since their offset/step is
|
||||||
|
// config-file-only, not per-profile (see qs_config.h's own comment).
|
||||||
|
JNIEXPORT jfloat JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetScreenWidthPresetValue(JNIEnv *env, jclass clazz,
|
||||||
|
jint index) {
|
||||||
|
return qs_config_screen_width_preset_value(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jfloat JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetScreenDistancePresetValue(JNIEnv *env, jclass clazz,
|
||||||
|
jint index) {
|
||||||
|
return qs_config_screen_distance_preset_value(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetLanguage(JNIEnv *env, jclass clazz) {
|
||||||
|
return qs_config_current_language();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuOverlay's header - polled on a periodic timer (see its own comment),
|
||||||
|
// not event-driven, so these are plain synchronous reads too.
|
||||||
|
JNIEXPORT jfloat JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetFps(JNIEnv *env, jclass clazz) {
|
||||||
|
return xr_session_get_fps();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jfloat JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetFrameTimeMs(JNIEnv *env, jclass clazz) {
|
||||||
|
return xr_session_get_frame_ms();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jfloat JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetCpuUtilization(JNIEnv *env, jclass clazz) {
|
||||||
|
return xr_session_get_cpu_utilization();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jfloat JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetGpuUtilization(JNIEnv *env, jclass clazz) {
|
||||||
|
return xr_session_get_gpu_utilization();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller Options tab: actionIndex follows xr_input.c's action order
|
||||||
|
// (MenuOverlay.java and Strings.java hold the matching labels).
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestBeginControlCapture(JNIEnv *env, jclass clazz,
|
||||||
|
jint actionIndex) {
|
||||||
|
qs_config_request_begin_control_capture(actionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestCancelControlCapture(JNIEnv *env, jclass clazz) {
|
||||||
|
qs_config_request_cancel_control_capture();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestResetPlayerControls(JNIEnv *env, jclass clazz) {
|
||||||
|
qs_config_request_reset_player_controls();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetControlBindingKey(JNIEnv *env, jclass clazz,
|
||||||
|
jint actionIndex) {
|
||||||
|
return (*env)->NewStringUTF(env, xr_input_get_action_binding_name(actionIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetActiveInteractionProfilePath(JNIEnv *env,
|
||||||
|
jclass clazz) {
|
||||||
|
return (*env)->NewStringUTF(env, xr_input_get_active_interaction_profile_path());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeSetControllerOptionsTabOpen(JNIEnv *env, jclass clazz,
|
||||||
|
jboolean open) {
|
||||||
|
xr_input_set_controller_options_tab_open(open == JNI_TRUE);
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,85 @@ 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 are standing and facing, from their current real head
|
||||||
|
// pose. Never reads or writes questshock.config.json. Called automatically
|
||||||
|
// once, from xr_input_sync_and_draw(), on the first frame with a valid head
|
||||||
|
// pose after startup, unless a complete saved player_position was loaded (see
|
||||||
|
// xr_input_skip_startup_recenter()): a saved position is relative to a
|
||||||
|
// previous session's real-space origin, which is not stable across launches.
|
||||||
|
// Returns false (no-op) if no valid head pose is available yet.
|
||||||
|
bool xr_input_recenter_player_to_screen(void);
|
||||||
|
|
||||||
|
// The Main Menu tab's "Reset Position" button (via qs_config.c). Like
|
||||||
|
// xr_input_recenter_player_to_screen(), but re-anchors only the player's
|
||||||
|
// position to the centre in front of the game quad; the current facing (yaw)
|
||||||
|
// is kept, so the screen does not rotate to follow the gaze. Returns false
|
||||||
|
// (no-op) if no valid head pose is available yet.
|
||||||
|
bool xr_input_reset_player_position(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);
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Remappable controls ("player_controls" config group; see qs_config.c and
|
||||||
|
// CONFIG.md). menu_toggle is not among them and has no getter/setter: it is
|
||||||
|
// fixed (see xr_input_sync_and_draw()).
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// Number of remappable actions and each one's index<->name mapping, which
|
||||||
|
// qs_config.c uses to parse/write the player_controls group without its own
|
||||||
|
// copy of the list. The names are the keys used in questshock.config.json;
|
||||||
|
// index_from_name() returns false for an unrecognized name.
|
||||||
|
int xr_input_action_count(void);
|
||||||
|
const char *xr_input_action_name(int action_index);
|
||||||
|
bool xr_input_action_index_from_name(const char *name, int *out_index);
|
||||||
|
|
||||||
|
// Current binding for one action, as an opaque int and as its string form
|
||||||
|
// (e.g. "right_b", "trigger", "none" for unbound), the same strings used in
|
||||||
|
// questshock.config.json and reported by capture.
|
||||||
|
int xr_input_get_action_binding(int action_index);
|
||||||
|
const char *xr_input_get_action_binding_name(int action_index);
|
||||||
|
|
||||||
|
// Sets one action's binding from a config-file string. Returns false (binding
|
||||||
|
// unchanged) for an unrecognized name, or for the symmetric "trigger"/
|
||||||
|
// "squeeze" on an action other than mouse_click/mouse_double_click. Does not
|
||||||
|
// enforce one-action-per-input the way capture does: a hand-edited file that
|
||||||
|
// binds two actions to one input is accepted as-is.
|
||||||
|
bool xr_input_set_action_binding(int action_index, const char *raw_name);
|
||||||
|
|
||||||
|
// Restores every action to its built-in default (the table xr_input_init()
|
||||||
|
// starts from). qs_config.c calls this first for "Reset to Defaults", then
|
||||||
|
// applies questshock.config.default.json's player_controls group on top if it
|
||||||
|
// has one.
|
||||||
|
void xr_input_reset_player_controls(void);
|
||||||
|
|
||||||
|
// Begins/cancels rebind capture for one action (an index into
|
||||||
|
// xr_input_action_name()'s list): the next fresh button or thumbstick-direction
|
||||||
|
// edge on any hand becomes that action's binding and is reported via
|
||||||
|
// qs_config_notify_control_capture_result(); with no press, capture cancels
|
||||||
|
// after 8 seconds. Called via qs_config.c's cross-thread request functions.
|
||||||
|
// The menu button is never captured.
|
||||||
|
void xr_input_capture_begin(int action_index);
|
||||||
|
void xr_input_capture_cancel(void);
|
||||||
|
|
||||||
|
// The OpenXR interaction profile currently bound (e.g.
|
||||||
|
// "/interaction_profiles/oculus/touch_controller"), refreshed once per frame.
|
||||||
|
// MenuOverlay.java's Controller Options tab uses it to pick a file from
|
||||||
|
// controller_profiles/ (see CONFIG.md). Empty until the first successful
|
||||||
|
// query.
|
||||||
|
const char *xr_input_get_active_interaction_profile_path(void);
|
||||||
|
|
||||||
|
// Set by MenuOverlay.java when the Controller Options tab becomes or stops
|
||||||
|
// being the menu's current tab. While it is open, the menu's thumbstick
|
||||||
|
// locomotion (snap turn/smooth move) is disabled.
|
||||||
|
void xr_input_set_controller_options_tab_open(bool open);
|
||||||
|
|
||||||
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,5 +1,7 @@
|
|||||||
#include "xr_session.h"
|
#include "xr_session.h"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <math.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -24,14 +26,18 @@
|
|||||||
#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__)
|
||||||
|
|
||||||
// Fixed pose/size for the single game quad - 2m in front of the local space
|
// Original, pre-screen-profile-feature default distance for the game
|
||||||
// origin, sized to preserve the game's own aspect ratio at a comfortable
|
// screen - 2m in front of the local space origin - now just the initial
|
||||||
// visual angle. No per-eye view/projection work is needed for a quad layer
|
// value of g_screen_distance_m (below) rather than a fixed constant.
|
||||||
// at all - the compositor handles reprojecting it per eye itself, which is
|
// Width/curve/distance are all configurable (see g_screen_width_m/
|
||||||
// exactly why this milestone doesn't need xrLocateViews or any stereo
|
// g_screen_curve_deg/g_screen_distance_m above and CONFIG.md's Video
|
||||||
// rendering (see the plan's step A notes).
|
// Options "Initial Screen"/"Game Screen" settings), sized to preserve the
|
||||||
|
// game's own pixel aspect ratio at whatever physical width is currently
|
||||||
|
// configured. No per-eye view/projection work is needed for a quad/
|
||||||
|
// cylinder layer at all - the compositor handles reprojecting it per eye
|
||||||
|
// itself, which is exactly why this milestone doesn't need xrLocateViews
|
||||||
|
// or any stereo rendering (see the plan's step A notes).
|
||||||
#define QUAD_DISTANCE_METERS 2.0f
|
#define QUAD_DISTANCE_METERS 2.0f
|
||||||
#define QUAD_WIDTH_METERS 1.6f
|
|
||||||
|
|
||||||
// The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is
|
// The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is
|
||||||
// small and sits above where the keyboard defaults to; the keyboard keeps
|
// small and sits above where the keyboard defaults to; the keyboard keeps
|
||||||
@@ -39,10 +45,22 @@
|
|||||||
// xr_overlay.h) so both can be shown/hidden/dragged separately - deliberately
|
// xr_overlay.h) so both can be shown/hidden/dragged separately - deliberately
|
||||||
// positioned apart by default so they don't start out overlapping.
|
// positioned apart by default so they don't start out overlapping.
|
||||||
#define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay"
|
#define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay"
|
||||||
#define MENU_WIDTH 512
|
// 768x800 (was 512x256, then 768x512, then 768x552) - grown to fit the
|
||||||
#define MENU_HEIGHT 256
|
// header/tabs/Video Options content added on top of the original single
|
||||||
|
// "Keyboard" button, then again to fit the stats ribbon below the tab
|
||||||
|
// bar, then again to fit the Video Options tab's screen-size grid; taller
|
||||||
|
// in world space too since MENU_WIDTH_METERS (the physical size) stays
|
||||||
|
// fixed and height_m is derived from it by aspect ratio (see
|
||||||
|
// xr_overlay.c's xr_overlay_init()). Must match MenuOverlay.java's own
|
||||||
|
// WIDTH/HEIGHT, and xr_input.c's MENU_TITLE_BAR_V_FRACTION denominator.
|
||||||
|
#define MENU_WIDTH 768
|
||||||
|
#define MENU_HEIGHT 800
|
||||||
#define MENU_DISTANCE_METERS 1.3f
|
#define MENU_DISTANCE_METERS 1.3f
|
||||||
#define MENU_WIDTH_METERS 0.35f
|
// Physical size - was 0.35m, too small to read comfortably at
|
||||||
|
// MENU_DISTANCE_METERS given how much text the header/tabs/stats ribbon
|
||||||
|
// pack in; same pixel resolution (MENU_WIDTH/HEIGHT above) just displayed
|
||||||
|
// bigger, not a layout change.
|
||||||
|
#define MENU_WIDTH_METERS 0.5f
|
||||||
#define MENU_DEFAULT_CENTER_X_METERS 0.0f
|
#define MENU_DEFAULT_CENTER_X_METERS 0.0f
|
||||||
#define MENU_DEFAULT_CENTER_Y_METERS 0.15f
|
#define MENU_DEFAULT_CENTER_Y_METERS 0.15f
|
||||||
|
|
||||||
@@ -58,6 +76,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*
|
||||||
@@ -79,6 +117,40 @@ static XrTime g_predicted_display_time = 0;
|
|||||||
static bool g_frame_should_render = false;
|
static bool g_frame_should_render = false;
|
||||||
static bool g_have_acquired_game_image = false;
|
static bool g_have_acquired_game_image = false;
|
||||||
|
|
||||||
|
// XR_META_performance_metrics support (see xr_session_init_performance_metrics()
|
||||||
|
// below) - only present if the runtime advertised it at xrCreateInstance
|
||||||
|
// time (checked via xr_instance_extension_supported()), since unlike the
|
||||||
|
// two always-required KHR extensions above, this one is a device-capability
|
||||||
|
// extra that a build shouldn't hard-depend on.
|
||||||
|
static bool g_performance_metrics_supported = false;
|
||||||
|
static PFN_xrSetPerformanceMetricsStateMETA g_xrSetPerformanceMetricsStateMETA = NULL;
|
||||||
|
static PFN_xrQueryPerformanceMetricsCounterMETA g_xrQueryPerformanceMetricsCounterMETA = NULL;
|
||||||
|
// Resolved once, at startup, by string-matching the runtime's own
|
||||||
|
// human-readable counter path names (see xr_session_init_performance_metrics())
|
||||||
|
// rather than hardcoding the exact path strings the spec defines - a
|
||||||
|
// substring match is robust to spec/runtime wording we can't verify without
|
||||||
|
// on-device testing. XR_NULL_PATH (0) if no match was found.
|
||||||
|
static XrPath g_cpu_utilization_path = XR_NULL_PATH;
|
||||||
|
static XrPath g_gpu_utilization_path = XR_NULL_PATH;
|
||||||
|
|
||||||
|
// XR_KHR_composition_layer_cylinder support - same "only if the runtime
|
||||||
|
// advertised it" reasoning as g_performance_metrics_supported above. If
|
||||||
|
// unsupported, the curve is forced to 0 (flat) everywhere regardless of
|
||||||
|
// configuration (see xr_get_game_quad_extent()) so the game screen just
|
||||||
|
// silently stays flat instead of failing to submit a layer at all.
|
||||||
|
static bool g_cylinder_layer_supported = false;
|
||||||
|
|
||||||
|
// The game screen's currently-configured shape (see qs_config.c's
|
||||||
|
// qs_screen_profile_notify_loop_mode(), which is what actually calls
|
||||||
|
// xr_session_set_game_screen_shape() below - this file has no notion of
|
||||||
|
// "which profile" is active, only "what shape is current"). Defaults
|
||||||
|
// match the game quad's own original, pre-this-feature hardcoded width
|
||||||
|
// (1.6m) and lack of curve, so a build that never touches the new config
|
||||||
|
// options renders identically to before.
|
||||||
|
static float g_screen_width_m = 1.6f;
|
||||||
|
static float g_screen_curve_deg = 0.0f;
|
||||||
|
static float g_screen_distance_m = QUAD_DISTANCE_METERS;
|
||||||
|
|
||||||
static bool xr_check(XrResult result, const char *what) {
|
static bool xr_check(XrResult result, const char *what) {
|
||||||
if (XR_SUCCEEDED(result))
|
if (XR_SUCCEEDED(result))
|
||||||
return true;
|
return true;
|
||||||
@@ -99,6 +171,106 @@ static PFN_xrVoidFunction xr_get_proc(const char *name) {
|
|||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Queried before xrCreateInstance (which is why this takes no instance
|
||||||
|
// handle - xrEnumerateInstanceExtensionProperties is one of the few
|
||||||
|
// functions that works without one) to decide whether it's safe to add
|
||||||
|
// `name` to enabledExtensionNames - passing an unsupported extension there
|
||||||
|
// would fail instance creation outright, unlike a missing optional feature
|
||||||
|
// discovered afterward.
|
||||||
|
static bool xr_instance_extension_supported(const char *name) {
|
||||||
|
uint32_t count = 0;
|
||||||
|
if (!xr_check(xrEnumerateInstanceExtensionProperties(NULL, 0, &count, NULL),
|
||||||
|
"xrEnumerateInstanceExtensionProperties"))
|
||||||
|
return false;
|
||||||
|
XrExtensionProperties *props = (XrExtensionProperties *)malloc(sizeof(XrExtensionProperties) * count);
|
||||||
|
for (uint32_t i = 0; i < count; i++)
|
||||||
|
props[i].type = XR_TYPE_EXTENSION_PROPERTIES;
|
||||||
|
bool supported = false;
|
||||||
|
if (xr_check(xrEnumerateInstanceExtensionProperties(NULL, count, &count, props),
|
||||||
|
"xrEnumerateInstanceExtensionProperties")) {
|
||||||
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
|
if (strcmp(props[i].extensionName, name) == 0) {
|
||||||
|
supported = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(props);
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case-insensitively checks whether `needle` occurs anywhere in `haystack` -
|
||||||
|
// used to identify a counter path (e.g. ".../device/cpu_utilization_average")
|
||||||
|
// by a short, stable substring instead of matching its exact full spelling.
|
||||||
|
static bool xr_path_name_contains(const char *haystack, const char *needle) {
|
||||||
|
size_t hlen = strlen(haystack);
|
||||||
|
char lower[256];
|
||||||
|
if (hlen >= sizeof(lower))
|
||||||
|
hlen = sizeof(lower) - 1;
|
||||||
|
for (size_t i = 0; i < hlen; i++)
|
||||||
|
lower[i] = (char)tolower((unsigned char)haystack[i]);
|
||||||
|
lower[hlen] = '\0';
|
||||||
|
return strstr(lower, needle) != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enables XR_META_performance_metrics (if the runtime advertised it - see
|
||||||
|
// xr_instance_extension_supported() above) and resolves the CPU/GPU
|
||||||
|
// device-utilization counter paths xr_session_update_frame_stats() polls
|
||||||
|
// every frame. Called once, right after xrCreateSession succeeds. Leaves
|
||||||
|
// g_cpu_utilization_path/g_gpu_utilization_path at XR_NULL_PATH (their
|
||||||
|
// getters then report -1.0f) if anything here doesn't pan out - this is a
|
||||||
|
// nice-to-have stats display, not something worth failing session bring-up
|
||||||
|
// over.
|
||||||
|
static void xr_session_init_performance_metrics(void) {
|
||||||
|
if (!g_performance_metrics_supported)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumeratePaths =
|
||||||
|
(PFN_xrEnumeratePerformanceMetricsCounterPathsMETA)xr_get_proc(
|
||||||
|
"xrEnumeratePerformanceMetricsCounterPathsMETA");
|
||||||
|
g_xrSetPerformanceMetricsStateMETA = (PFN_xrSetPerformanceMetricsStateMETA)xr_get_proc(
|
||||||
|
"xrSetPerformanceMetricsStateMETA");
|
||||||
|
g_xrQueryPerformanceMetricsCounterMETA = (PFN_xrQueryPerformanceMetricsCounterMETA)xr_get_proc(
|
||||||
|
"xrQueryPerformanceMetricsCounterMETA");
|
||||||
|
if (enumeratePaths == NULL || g_xrSetPerformanceMetricsStateMETA == NULL ||
|
||||||
|
g_xrQueryPerformanceMetricsCounterMETA == NULL)
|
||||||
|
return;
|
||||||
|
|
||||||
|
XrPerformanceMetricsStateMETA state = {XR_TYPE_PERFORMANCE_METRICS_STATE_META};
|
||||||
|
state.enabled = XR_TRUE;
|
||||||
|
if (!xr_check(g_xrSetPerformanceMetricsStateMETA(g_session, &state),
|
||||||
|
"xrSetPerformanceMetricsStateMETA"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint32_t pathCount = 0;
|
||||||
|
if (!xr_check(enumeratePaths(g_instance, 0, &pathCount, NULL),
|
||||||
|
"xrEnumeratePerformanceMetricsCounterPathsMETA"))
|
||||||
|
return;
|
||||||
|
XrPath *paths = (XrPath *)malloc(sizeof(XrPath) * pathCount);
|
||||||
|
if (!xr_check(enumeratePaths(g_instance, pathCount, &pathCount, paths),
|
||||||
|
"xrEnumeratePerformanceMetricsCounterPathsMETA")) {
|
||||||
|
free(paths);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < pathCount; i++) {
|
||||||
|
char pathName[256];
|
||||||
|
uint32_t pathNameLen = 0;
|
||||||
|
if (!xr_check(xrPathToString(g_instance, paths[i], sizeof(pathName), &pathNameLen, pathName),
|
||||||
|
"xrPathToString"))
|
||||||
|
continue;
|
||||||
|
if (g_cpu_utilization_path == XR_NULL_PATH && xr_path_name_contains(pathName, "cpu_utilization"))
|
||||||
|
g_cpu_utilization_path = paths[i];
|
||||||
|
if (g_gpu_utilization_path == XR_NULL_PATH && xr_path_name_contains(pathName, "gpu_utilization"))
|
||||||
|
g_gpu_utilization_path = paths[i];
|
||||||
|
}
|
||||||
|
free(paths);
|
||||||
|
|
||||||
|
LOGI("XR: performance metrics cpu_utilization=%s gpu_utilization=%s",
|
||||||
|
g_cpu_utilization_path != XR_NULL_PATH ? "found" : "not found",
|
||||||
|
g_gpu_utilization_path != XR_NULL_PATH ? "found" : "not found");
|
||||||
|
}
|
||||||
|
|
||||||
// The Android OpenXR loader needs the JavaVM/context before anything else -
|
// The Android OpenXR loader needs the JavaVM/context before anything else -
|
||||||
// separate from XR_KHR_android_create_instance below, which only affects
|
// separate from XR_KHR_android_create_instance below, which only affects
|
||||||
// xrCreateInstance itself. Skipping this is a common cause of
|
// xrCreateInstance itself. Skipping this is a common cause of
|
||||||
@@ -133,6 +305,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();
|
||||||
@@ -146,10 +377,26 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
|||||||
if (!xr_initialize_android_loader(vm, activity))
|
if (!xr_initialize_android_loader(vm, activity))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const char *extensions[] = {
|
g_performance_metrics_supported =
|
||||||
|
xr_instance_extension_supported(XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
|
||||||
|
g_cylinder_layer_supported =
|
||||||
|
xr_instance_extension_supported(XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME);
|
||||||
|
|
||||||
|
// The two KHR extensions here are always required; the other two are
|
||||||
|
// each independently optional device capabilities (see
|
||||||
|
// g_performance_metrics_supported/g_cylinder_layer_supported's own
|
||||||
|
// comments) - appended only if actually advertised, since listing an
|
||||||
|
// unsupported extension fails xrCreateInstance entirely, not just that
|
||||||
|
// one feature.
|
||||||
|
const char *extensions[4] = {
|
||||||
XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME,
|
XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME,
|
||||||
XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME,
|
XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME,
|
||||||
};
|
};
|
||||||
|
uint32_t extensionCount = 2;
|
||||||
|
if (g_performance_metrics_supported)
|
||||||
|
extensions[extensionCount++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
|
||||||
|
if (g_cylinder_layer_supported)
|
||||||
|
extensions[extensionCount++] = XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME;
|
||||||
|
|
||||||
XrInstanceCreateInfoAndroidKHR androidInfo = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
|
XrInstanceCreateInfoAndroidKHR androidInfo = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
|
||||||
androidInfo.applicationVM = vm;
|
androidInfo.applicationVM = vm;
|
||||||
@@ -157,7 +404,7 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
|||||||
|
|
||||||
XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO};
|
XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO};
|
||||||
createInfo.next = &androidInfo;
|
createInfo.next = &androidInfo;
|
||||||
createInfo.enabledExtensionCount = sizeof(extensions) / sizeof(extensions[0]);
|
createInfo.enabledExtensionCount = extensionCount;
|
||||||
createInfo.enabledExtensionNames = extensions;
|
createInfo.enabledExtensionNames = extensions;
|
||||||
strncpy(createInfo.applicationInfo.applicationName, "QuestShock",
|
strncpy(createInfo.applicationInfo.applicationName, "QuestShock",
|
||||||
XR_MAX_APPLICATION_NAME_SIZE - 1);
|
XR_MAX_APPLICATION_NAME_SIZE - 1);
|
||||||
@@ -206,6 +453,8 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
|||||||
if (!xr_check(xrCreateSession(g_instance, &sessionInfo, &g_session), "xrCreateSession"))
|
if (!xr_check(xrCreateSession(g_instance, &sessionInfo, &g_session), "xrCreateSession"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
xr_session_init_performance_metrics();
|
||||||
|
|
||||||
XrReferenceSpaceCreateInfo spaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
XrReferenceSpaceCreateInfo spaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||||
spaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
spaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||||
spaceInfo.poseInReferenceSpace.orientation.w = 1.0f;
|
spaceInfo.poseInReferenceSpace.orientation.w = 1.0f;
|
||||||
@@ -213,6 +462,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
|
||||||
@@ -319,6 +576,14 @@ void xr_poll_events(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xrPollEvent() leaves gl4es reporting a stray GL_INVALID_OPERATION on
|
||||||
|
// every call while the session is running - drain it (looped, since
|
||||||
|
// more than one can accumulate across xrPollEvent()'s possibly-multiple
|
||||||
|
// iterations above) so it doesn't surface as a false positive at the
|
||||||
|
// next unrelated glGetError() check in this app's own rendering code.
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool xr_is_session_running(void) { return g_session_running; }
|
bool xr_is_session_running(void) { return g_session_running; }
|
||||||
@@ -346,10 +611,77 @@ bool xr_frame_begin(void) {
|
|||||||
return g_have_acquired_game_image;
|
return g_have_acquired_game_image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backs xr_session_get_fps()/get_frame_ms() (see xr_session.h) - exponential
|
||||||
|
// smoothing (rather than a plain per-frame instantaneous value) so
|
||||||
|
// MenuOverlay's header display doesn't visibly jitter every single frame.
|
||||||
|
// Called once per xr_frame_end() that reaches its own end (i.e. once per
|
||||||
|
// real XR frame, whether or not the game itself actually rendered anything
|
||||||
|
// new into it this frame - matches the OpenXR compositor's own frame
|
||||||
|
// cadence, which is what "FPS" means to someone looking at this display).
|
||||||
|
static float s_fps = 0.0f;
|
||||||
|
static float s_frame_ms = 0.0f;
|
||||||
|
static float s_cpu_utilization = -1.0f;
|
||||||
|
static float s_gpu_utilization = -1.0f;
|
||||||
|
|
||||||
|
// Reads one XR_META_performance_metrics counter (if it was found at
|
||||||
|
// startup - see xr_session_init_performance_metrics()) and folds it into
|
||||||
|
// *smoothed with the same exponential smoothing as FPS/frame time above.
|
||||||
|
// Leaves *smoothed at -1.0f (its "unsupported" sentinel) untouched if the
|
||||||
|
// path wasn't found, the query fails, or the runtime didn't mark its
|
||||||
|
// floatValue valid this call.
|
||||||
|
static void xr_session_update_utilization(XrPath path, float *smoothed) {
|
||||||
|
if (path == XR_NULL_PATH || g_xrQueryPerformanceMetricsCounterMETA == NULL)
|
||||||
|
return;
|
||||||
|
XrPerformanceMetricsCounterMETA counter = {XR_TYPE_PERFORMANCE_METRICS_COUNTER_META};
|
||||||
|
if (!xr_check(g_xrQueryPerformanceMetricsCounterMETA(g_session, path, &counter),
|
||||||
|
"xrQueryPerformanceMetricsCounterMETA"))
|
||||||
|
return;
|
||||||
|
if (!(counter.counterFlags & XR_PERFORMANCE_METRICS_COUNTER_FLOAT_VALUE_VALID_BIT_META))
|
||||||
|
return;
|
||||||
|
const float kSmoothing = 0.1f;
|
||||||
|
*smoothed =
|
||||||
|
(*smoothed < 0.0f) ? counter.floatValue : *smoothed * (1.0f - kSmoothing) + counter.floatValue * kSmoothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void xr_session_update_frame_stats(void) {
|
||||||
|
static Uint32 s_last_ticks = 0;
|
||||||
|
Uint32 now = SDL_GetTicks();
|
||||||
|
if (s_last_ticks != 0) {
|
||||||
|
Uint32 delta_ms = now - s_last_ticks;
|
||||||
|
if (delta_ms > 0) {
|
||||||
|
float instant_fps = 1000.0f / (float)delta_ms;
|
||||||
|
const float kSmoothing = 0.1f;
|
||||||
|
s_fps = (s_fps == 0.0f) ? instant_fps : s_fps * (1.0f - kSmoothing) + instant_fps * kSmoothing;
|
||||||
|
s_frame_ms =
|
||||||
|
(s_frame_ms == 0.0f) ? (float)delta_ms : s_frame_ms * (1.0f - kSmoothing) + (float)delta_ms * kSmoothing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s_last_ticks = now;
|
||||||
|
|
||||||
|
xr_session_update_utilization(g_cpu_utilization_path, &s_cpu_utilization);
|
||||||
|
xr_session_update_utilization(g_gpu_utilization_path, &s_gpu_utilization);
|
||||||
|
}
|
||||||
|
|
||||||
|
float xr_session_get_fps(void) { return s_fps; }
|
||||||
|
|
||||||
|
float xr_session_get_frame_ms(void) { return s_frame_ms; }
|
||||||
|
|
||||||
|
float xr_session_get_cpu_utilization(void) { return s_cpu_utilization; }
|
||||||
|
|
||||||
|
float xr_session_get_gpu_utilization(void) { return s_gpu_utilization; }
|
||||||
|
|
||||||
void xr_frame_end(void) {
|
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
|
||||||
@@ -358,7 +690,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)
|
||||||
@@ -367,34 +699,58 @@ void xr_frame_end(void) {
|
|||||||
if (!xr_is_session_running())
|
if (!xr_is_session_running())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Up to 5 layers: the game quad (if a frame was actually rendered),
|
// Up to 5 layers: the game screen (if a frame was actually rendered) -
|
||||||
// the menu launcher and keyboard overlays while visible - each gets
|
// a flat quad, or a curved cylinder if the currently-active screen
|
||||||
// its own acquire/render/release cycle against its own swapchain (see
|
// profile has a nonzero curve (see xr_get_game_quad_extent()'s own
|
||||||
// xr_overlay_render_and_build_layer()) any time before xrEndFrame,
|
// comment; xr_input.c's xr_input_try_game_quad() hit-tests whichever
|
||||||
// unlike the game quad there's no per-frame engine rendering to wrap
|
// shape is actually submitted here, always agreeing since both read
|
||||||
// around here, just each overlay's own blit - and, per hand, a
|
// the same getter) - the menu launcher and keyboard overlays while
|
||||||
// laser-beam quad while that hand's ray is aimed at the keyboard,
|
// visible - each gets its own acquire/render/release cycle against
|
||||||
// menu, or the game quad itself (see xr_input_get_beam_layer()).
|
// its own swapchain (see xr_overlay_render_and_build_layer()) any
|
||||||
|
// time before xrEndFrame, unlike the game screen there's no per-frame
|
||||||
|
// engine rendering to wrap around here, just each overlay's own blit -
|
||||||
|
// and, per hand, a laser-beam quad while that hand's ray is aimed at
|
||||||
|
// the keyboard, menu, or the game screen itself (see
|
||||||
|
// xr_input_get_beam_layer()).
|
||||||
|
float gameDistance, gameHalfWidth, gameHalfHeight, gameRadius, gameCentralAngle;
|
||||||
|
xr_get_game_quad_extent(&gameDistance, &gameHalfWidth, &gameHalfHeight, &gameRadius,
|
||||||
|
&gameCentralAngle);
|
||||||
|
bool gameScreenCurved = gameCentralAngle > 0.0f;
|
||||||
|
|
||||||
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||||
gameQuad.space = g_local_space;
|
XrCompositionLayerCylinderKHR gameCylinder = {XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR};
|
||||||
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
if (gameScreenCurved) {
|
||||||
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
gameCylinder.space = g_play_space;
|
||||||
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
gameCylinder.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||||
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
|
gameCylinder.subImage.swapchain = g_game_swapchain.swapchain;
|
||||||
gameQuad.pose.orientation.w = 1.0f;
|
gameCylinder.subImage.imageRect.extent.width = g_game_swapchain.width;
|
||||||
gameQuad.pose.position.z = -QUAD_DISTANCE_METERS;
|
gameCylinder.subImage.imageRect.extent.height = g_game_swapchain.height;
|
||||||
gameQuad.size.width = QUAD_WIDTH_METERS;
|
gameCylinder.pose.orientation.w = 1.0f;
|
||||||
gameQuad.size.height =
|
gameCylinder.pose.position.z = -gameDistance;
|
||||||
QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
|
gameCylinder.radius = gameRadius;
|
||||||
|
gameCylinder.centralAngle = gameCentralAngle;
|
||||||
|
gameCylinder.aspectRatio = (float)g_game_swapchain.width / (float)g_game_swapchain.height;
|
||||||
|
} else {
|
||||||
|
gameQuad.space = g_play_space;
|
||||||
|
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||||
|
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
||||||
|
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
||||||
|
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
|
||||||
|
gameQuad.pose.orientation.w = 1.0f;
|
||||||
|
gameQuad.pose.position.z = -gameDistance;
|
||||||
|
gameQuad.size.width = gameHalfWidth * 2.0f;
|
||||||
|
gameQuad.size.height = gameHalfHeight * 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
const XrCompositionLayerBaseHeader *layers[5];
|
const XrCompositionLayerBaseHeader *layers[5];
|
||||||
uint32_t layerCount = 0;
|
uint32_t layerCount = 0;
|
||||||
if (g_have_acquired_game_image)
|
if (g_have_acquired_game_image)
|
||||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad;
|
layers[layerCount++] = gameScreenCurved ? (XrCompositionLayerBaseHeader *)&gameCylinder
|
||||||
|
: (XrCompositionLayerBaseHeader *)&gameQuad;
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -402,7 +758,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
|
||||||
@@ -422,7 +778,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];
|
||||||
@@ -434,7 +790,10 @@ void xr_frame_end(void) {
|
|||||||
endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||||
endInfo.layerCount = layerCount;
|
endInfo.layerCount = layerCount;
|
||||||
endInfo.layers = layerCount > 0 ? layers : NULL;
|
endInfo.layers = layerCount > 0 ? layers : NULL;
|
||||||
|
|
||||||
xr_check(xrEndFrame(g_session, &endInfo), "xrEndFrame");
|
xr_check(xrEndFrame(g_session, &endInfo), "xrEndFrame");
|
||||||
|
|
||||||
|
xr_session_update_frame_stats();
|
||||||
}
|
}
|
||||||
|
|
||||||
void xr_shutdown(void) {
|
void xr_shutdown(void) {
|
||||||
@@ -444,6 +803,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);
|
||||||
@@ -461,16 +824,69 @@ void xr_shutdown(void) {
|
|||||||
|
|
||||||
g_session_state = XR_SESSION_STATE_UNKNOWN;
|
g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||||
g_session_running = false;
|
g_session_running = false;
|
||||||
|
|
||||||
|
g_performance_metrics_supported = false;
|
||||||
|
g_xrSetPerformanceMetricsStateMETA = NULL;
|
||||||
|
g_xrQueryPerformanceMetricsCounterMETA = NULL;
|
||||||
|
g_cpu_utilization_path = XR_NULL_PATH;
|
||||||
|
g_gpu_utilization_path = XR_NULL_PATH;
|
||||||
|
|
||||||
|
g_cylinder_layer_supported = false;
|
||||||
|
// g_screen_width_m/g_screen_curve_deg/g_screen_distance_m deliberately
|
||||||
|
// NOT reset here - same reasoning as xr_session_set_player_transform()'s
|
||||||
|
// own state: it's meant to persist across a shutdown/reinit within the
|
||||||
|
// same process (e.g. an Android lifecycle pause/resume), not just
|
||||||
|
// within one XR session.
|
||||||
}
|
}
|
||||||
|
|
||||||
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m) {
|
void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m) {
|
||||||
*distance_m = QUAD_DISTANCE_METERS;
|
g_screen_width_m = width_m;
|
||||||
*half_width_m = QUAD_WIDTH_METERS * 0.5f;
|
g_screen_curve_deg = curve_deg;
|
||||||
*half_height_m =
|
g_screen_distance_m = distance_m;
|
||||||
QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
|
}
|
||||||
|
|
||||||
|
// Single source of truth for the game screen's current shape, read by
|
||||||
|
// both xr_frame_end() (to build/submit the actual layer) and
|
||||||
|
// xr_input_try_game_quad() (xr_input.c, to hit-test the laser pointer
|
||||||
|
// against it). half_width_m is meaningful only when *central_angle_rad is
|
||||||
|
// 0 (flat); a curved screen's horizontal extent is bounded by
|
||||||
|
// *central_angle_rad instead, with *half_height_m always the vertical
|
||||||
|
// bound either way.
|
||||||
|
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m,
|
||||||
|
float *radius_m, float *central_angle_rad) {
|
||||||
|
float curveDeg = g_cylinder_layer_supported ? g_screen_curve_deg : 0.0f;
|
||||||
|
float aspect = (float)g_game_swapchain.height / (float)g_game_swapchain.width;
|
||||||
|
|
||||||
|
*distance_m = g_screen_distance_m;
|
||||||
|
*half_width_m = g_screen_width_m * 0.5f;
|
||||||
|
*half_height_m = g_screen_width_m * 0.5f * aspect;
|
||||||
|
|
||||||
|
if (curveDeg > 0.0f) {
|
||||||
|
float theta = curveDeg * QS_DEG_TO_RAD;
|
||||||
|
*central_angle_rad = theta;
|
||||||
|
*radius_m = g_screen_width_m / theta;
|
||||||
|
} else {
|
||||||
|
*central_angle_rad = 0.0f;
|
||||||
|
*radius_m = 0.0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void xr_get_game_resolution(int *width, int *height) {
|
void xr_get_game_resolution(int *width, int *height) {
|
||||||
*width = g_game_swapchain.width;
|
// Not g_game_swapchain.width/height: that's the fixed physical size of
|
||||||
*height = g_game_swapchain.height;
|
// the swapchain image the game is composited into (see xr_init()) -
|
||||||
|
// android_draw_surface_as_quad() (OpenGL.cc, engine-patches/11) always
|
||||||
|
// stretch-blits drawSurface to fill that image regardless of
|
||||||
|
// drawSurface's own pixel size, so the swapchain's size has no relation
|
||||||
|
// to the coordinate space MouseX/MouseY actually live in. gScreenWide/
|
||||||
|
// gScreenHigh (InitMac.h) is drawSurface's current size - the
|
||||||
|
// resolution the in-game options menu can change at runtime
|
||||||
|
// (ShockBitmap.c's ChangeScreenSize()) independently of the swapchain,
|
||||||
|
// which is created once at launch and never resized. Using the fixed
|
||||||
|
// swapchain size here after such a change mapped the laser pointer's
|
||||||
|
// quad-space hit into the wrong coordinate space entirely, moving
|
||||||
|
// MouseX/MouseY out of the engine's actual screen bounds - which is
|
||||||
|
// what made the cursor disappear and become unclickable.
|
||||||
|
extern int32_t gScreenWide, gScreenHigh;
|
||||||
|
*width = gScreenWide;
|
||||||
|
*height = gScreenHigh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,25 +62,104 @@ 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 screen 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. *radius_m/
|
||||||
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m);
|
// *central_angle_rad are 0 when flat, in which case *half_width_m is the
|
||||||
|
// horizontal extent; otherwise the screen is a curved cylinder segment
|
||||||
|
// and the caller uses *central_angle_rad instead of *half_width_m for the
|
||||||
|
// horizontal bound (*half_height_m stays meaningful either way). Curving
|
||||||
|
// is currently unused - qs_config.c has no config surface for it - but
|
||||||
|
// kept working so it can be re-enabled later.
|
||||||
|
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m,
|
||||||
|
float *radius_m, float *central_angle_rad);
|
||||||
|
|
||||||
// Pixel resolution of the game quad's own swapchain - i.e. game_width/
|
// Sets the game screen's current physical width (meters) and curve (wrap
|
||||||
// game_height as passed to xr_init() (grd_cap->w/h, see Shock.c's
|
// angle in degrees, 0 = flat) - a plain state setter, no XR calls, same
|
||||||
// InitSDL()) - the logical coordinate space xr_input.c needs to convert a
|
// category as xr_session_set_player_transform() above (safe to call
|
||||||
// game-quad ray hit's normalized u,v into engine mouse coordinates (see
|
// before xr_init() has even run). Read by both xr_frame_end() (to build/
|
||||||
// xr_mouse.h).
|
// submit the actual layer) and, via xr_get_game_quad_extent() above,
|
||||||
|
// xr_input.c's hit-testing. Driven by qs_config.c's
|
||||||
|
// qs_screen_profile_notify_loop_mode(), which decides which of the
|
||||||
|
// "Initial Screen"/"Game Screen" config profiles (see CONFIG.md) is
|
||||||
|
// currently active - this function itself has no notion of "profiles",
|
||||||
|
// only "what shape is current".
|
||||||
|
void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m);
|
||||||
|
|
||||||
|
// The engine's current screen resolution (gScreenWide/gScreenHigh,
|
||||||
|
// InitMac.h) - not the game quad's own swapchain size, which is fixed at
|
||||||
|
// launch and never changes - the logical coordinate space xr_input.c needs
|
||||||
|
// to convert a game-quad ray hit's normalized u,v into engine mouse
|
||||||
|
// coordinates (see xr_mouse.h). Can change at runtime via the in-game
|
||||||
|
// options menu (ShockBitmap.c's ChangeScreenSize()), independently of the
|
||||||
|
// swapchain.
|
||||||
void xr_get_game_resolution(int *width, int *height);
|
void xr_get_game_resolution(int *width, int *height);
|
||||||
|
|
||||||
|
// Exponentially-smoothed frame rate/frame time, updated once per real
|
||||||
|
// rendered frame in xr_frame_end() - read by MenuOverlay's header (see
|
||||||
|
// questshock_native.c's nativeGetFps()/nativeGetFrameTimeMs()). 0 before
|
||||||
|
// the second frame has run (nothing to compute a delta from yet).
|
||||||
|
float xr_session_get_fps(void);
|
||||||
|
float xr_session_get_frame_ms(void);
|
||||||
|
|
||||||
|
// Device-wide CPU/GPU utilization percentage (0-100), via the
|
||||||
|
// XR_META_performance_metrics extension - same per-frame update cadence as
|
||||||
|
// the FPS/frame-time getters above. Returns -1.0f if the extension isn't
|
||||||
|
// supported by the runtime, or its cpu_utilization/gpu_utilization counter
|
||||||
|
// couldn't be found among the ones it advertises.
|
||||||
|
float xr_session_get_cpu_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
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ bool xr_swapchain_acquire(XrInstance instance, XrSwapchainState *state) {
|
|||||||
// (android_draw_surface_as_quad()) land in the right place.
|
// (android_draw_surface_as_quad()) land in the right place.
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, state->fbos[imageIndex]);
|
glBindFramebuffer(GL_FRAMEBUFFER, state->fbos[imageIndex]);
|
||||||
glViewport(0, 0, state->width, state->height);
|
glViewport(0, 0, state->width, state->height);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,11 +59,12 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
private static final int TYPED_TEXT_HEIGHT = 100;
|
private static final int TYPED_TEXT_HEIGHT = 100;
|
||||||
|
|
||||||
// Height of the title bar (buildTitleBar()) and width of its Close
|
// Height of the title bar (buildTitleBar()) and width of its Close
|
||||||
// button - xr_input.c's TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION
|
// button - xr_input.c's KEYBOARD_TITLE_BAR_V_FRACTION/
|
||||||
// must be kept in sync with these (as fractions of WIDTH/HEIGHT above),
|
// KEYBOARD_CLOSE_BUTTON_U_FRACTION must be kept in sync with these (as
|
||||||
// since the drag-handle-vs-Close-button-vs-rest-of-the-keyboard
|
// fractions of WIDTH/HEIGHT above), since the drag-handle-vs-Close-
|
||||||
// decision happens entirely on the native side, before a hit ever
|
// button-vs-rest-of-the-keyboard decision happens entirely on the
|
||||||
// reaches this class's own touch dispatch.
|
// native side, before a hit ever reaches this class's own touch
|
||||||
|
// dispatch.
|
||||||
private static final int TITLE_BAR_HEIGHT = 90;
|
private static final int TITLE_BAR_HEIGHT = 90;
|
||||||
private static final int CLOSE_BUTTON_WIDTH = 160;
|
private static final int CLOSE_BUTTON_WIDTH = 160;
|
||||||
|
|
||||||
@@ -76,6 +77,17 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
private TextView typedTextView;
|
private TextView typedTextView;
|
||||||
private final StringBuilder typedText = new StringBuilder();
|
private final StringBuilder typedText = new StringBuilder();
|
||||||
|
|
||||||
|
// Title bar chrome text - the only pieces of this panel that get
|
||||||
|
// translated (see Strings.java's own comment on why the actual key
|
||||||
|
// labels don't). Set from MenuOverlay.nativeGetLanguage() at the top of
|
||||||
|
// buildContent() (see OverlayPanel's constructor note on why a field a
|
||||||
|
// subclass's buildContent() depends on can't rely on normal field-
|
||||||
|
// initializer timing) and kept in sync afterward by applyTranslations()
|
||||||
|
// below, called from MenuOverlay's own periodic language-refresh check.
|
||||||
|
private int currentLanguage = Strings.ENGLISH;
|
||||||
|
private TextView titleLabel;
|
||||||
|
private Button closeButton;
|
||||||
|
|
||||||
// Shift is a local-only layer toggle (never itself sent to the game -
|
// Shift is a local-only layer toggle (never itself sent to the game -
|
||||||
// see updateShiftVisual()); Ctrl/Alt are one-shot "armed" modifiers,
|
// see updateShiftVisual()); Ctrl/Alt are one-shot "armed" modifiers,
|
||||||
// sent to the game the moment they're pressed and released again as
|
// sent to the game the moment they're pressed and released again as
|
||||||
@@ -142,6 +154,7 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
@Override
|
@Override
|
||||||
protected View buildContent() {
|
protected View buildContent() {
|
||||||
charKeys = new ArrayList<>();
|
charKeys = new ArrayList<>();
|
||||||
|
currentLanguage = MenuOverlay.nativeGetLanguage();
|
||||||
|
|
||||||
LinearLayout panel = new LinearLayout(activity);
|
LinearLayout panel = new LinearLayout(activity);
|
||||||
panel.setOrientation(LinearLayout.VERTICAL);
|
panel.setOrientation(LinearLayout.VERTICAL);
|
||||||
@@ -214,26 +227,31 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
// xr_input.c before a hit ever reaches here) plus a real Close button,
|
// xr_input.c before a hit ever reaches here) plus a real Close button,
|
||||||
// which is dispatched as an ordinary click like any other key. Its
|
// which is dispatched as an ordinary click like any other key. Its
|
||||||
// height and the Close button's width must stay in sync with
|
// height and the Close button's width must stay in sync with
|
||||||
// xr_input.c's TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION.
|
// xr_input.c's KEYBOARD_TITLE_BAR_V_FRACTION/
|
||||||
|
// KEYBOARD_CLOSE_BUTTON_U_FRACTION.
|
||||||
private LinearLayout buildTitleBar() {
|
private LinearLayout buildTitleBar() {
|
||||||
LinearLayout bar = new LinearLayout(activity);
|
LinearLayout bar = new LinearLayout(activity);
|
||||||
bar.setOrientation(LinearLayout.HORIZONTAL);
|
bar.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
bar.setBackgroundColor(0xFF3A3A3A);
|
// Matches MenuOverlay's own header bar style.
|
||||||
|
bar.setBackgroundColor(0xFF000080);
|
||||||
|
|
||||||
TextView label = new TextView(activity);
|
titleLabel = new TextView(activity);
|
||||||
label.setText("Keyboard - drag here to move");
|
titleLabel.setText(Strings.get("keyboard_drag_title", currentLanguage));
|
||||||
label.setTextColor(0xFFFFFFFF);
|
titleLabel.setTextColor(0xFFFFFFFF);
|
||||||
label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 22);
|
titleLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||||
label.setGravity(Gravity.CENTER_VERTICAL);
|
titleLabel.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
label.setPadding(24, 0, 0, 0);
|
titleLabel.setPadding(24, 0, 0, 0);
|
||||||
bar.addView(label, new LinearLayout.LayoutParams(
|
bar.addView(titleLabel, new LinearLayout.LayoutParams(
|
||||||
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||||
|
|
||||||
Button close = new Button(activity);
|
closeButton = new Button(activity);
|
||||||
close.setText("Close");
|
closeButton.setText(Strings.get("close", currentLanguage));
|
||||||
close.setOnClickListener(v -> nativeRequestClose());
|
closeButton.setOnClickListener(v -> nativeRequestClose());
|
||||||
close.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
closeButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||||
bar.addView(close,
|
closeButton.setBackground(buttonBackground());
|
||||||
|
closeButton.setTextColor(buttonTextColors());
|
||||||
|
registerHoverable(closeButton);
|
||||||
|
bar.addView(closeButton,
|
||||||
new LinearLayout.LayoutParams(CLOSE_BUTTON_WIDTH, ViewGroup.LayoutParams.MATCH_PARENT));
|
new LinearLayout.LayoutParams(CLOSE_BUTTON_WIDTH, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
return bar;
|
return bar;
|
||||||
@@ -253,6 +271,9 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
for (Object item : items) {
|
for (Object item : items) {
|
||||||
Button key = new Button(activity);
|
Button key = new Button(activity);
|
||||||
|
key.setBackground(buttonBackground());
|
||||||
|
key.setTextColor(buttonTextColors());
|
||||||
|
registerHoverable(key);
|
||||||
float weight;
|
float weight;
|
||||||
if (item instanceof CharKey) {
|
if (item instanceof CharKey) {
|
||||||
CharKey charKey = (CharKey) item;
|
CharKey charKey = (CharKey) item;
|
||||||
@@ -517,6 +538,26 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
// the keyboard (see this class's doc comment and xr_input.c).
|
// the keyboard (see this class's doc comment and xr_input.c).
|
||||||
private static native void nativeRequestClose();
|
private static native void nativeRequestClose();
|
||||||
|
|
||||||
|
// Called from MenuOverlay's own periodic header-refresh timer when it
|
||||||
|
// detects the game's language preference changed mid-session (via its
|
||||||
|
// own options menu) - re-labels this panel's title bar chrome to match.
|
||||||
|
// A plain Java call (both classes are in this same package), not JNI -
|
||||||
|
// runOnUiThread() here is defensive/self-documenting more than
|
||||||
|
// strictly needed today, since MenuOverlay's own timer already runs on
|
||||||
|
// the UI thread itself (Activity.runOnUiThread() is a no-op wrapper
|
||||||
|
// when already on that thread).
|
||||||
|
public static void applyTranslations(final int language) {
|
||||||
|
if (instance == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instance.activity.runOnUiThread(() -> {
|
||||||
|
instance.currentLanguage = language;
|
||||||
|
instance.titleLabel.setText(Strings.get("keyboard_drag_title", language));
|
||||||
|
instance.closeButton.setText(Strings.get("close", language));
|
||||||
|
instance.redrawContent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Called from xr_overlay.c's xr_overlay_init_jni(), on the render
|
// Called from xr_overlay.c's xr_overlay_init_jni(), on the render
|
||||||
// thread, right after it resolves this class via the activity's own
|
// thread, right after it resolves this class via the activity's own
|
||||||
// ClassLoader (plain FindClass() can't see app classes from a thread
|
// ClassLoader (plain FindClass() can't see app classes from a thread
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
package de.ladkau.questshock;
|
package de.ladkau.questshock;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.graphics.drawable.StateListDrawable;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared machinery for an off-screen, never-attached-to-a-window View tree
|
* Shared machinery for an off-screen, never-attached-to-a-window View tree
|
||||||
@@ -59,11 +66,59 @@ abstract class OverlayPanel {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A plain, instantly-drawn StateListDrawable rather than the platform
|
||||||
|
// default Button background (a Material ripple): ripples animate via
|
||||||
|
// continuous invalidation callbacks tied to a real attached window's
|
||||||
|
// vsync, which never fires here (this tree is never attached to a
|
||||||
|
// window) - so setPressed()/setHovered() state changes would otherwise
|
||||||
|
// produce no visible change at all, independent of this project's own
|
||||||
|
// touch handling (which already calls setPressed() correctly - see
|
||||||
|
// CLICK_ON_TOUCH_UP above). Every button this project creates
|
||||||
|
// (MenuOverlay, KeyboardOverlay) should use this (or
|
||||||
|
// MenuOverlay.selectableButtonBackground(), which adds a persistent
|
||||||
|
// "selected" state on top of the same normal/hovered/pressed colors) -
|
||||||
|
// paired with buttonTextColors() below, since the normal/hovered state
|
||||||
|
// is light enough to need dark text while pressed/selected need white.
|
||||||
|
protected static Drawable buttonBackground() {
|
||||||
|
StateListDrawable drawable = new StateListDrawable();
|
||||||
|
drawable.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(PRESSED_COLOR));
|
||||||
|
drawable.addState(new int[]{android.R.attr.state_hovered}, new ColorDrawable(HOVERED_COLOR));
|
||||||
|
drawable.addState(new int[]{}, new ColorDrawable(NORMAL_COLOR));
|
||||||
|
return drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See buttonBackground() above - light gray (normal/hovered) needs dark
|
||||||
|
// text; the accent colors (pressed, and MenuOverlay's own "selected"
|
||||||
|
// state) need white. Every button should get both this and
|
||||||
|
// buttonBackground()/selectableButtonBackground() together.
|
||||||
|
protected static ColorStateList buttonTextColors() {
|
||||||
|
return new ColorStateList(
|
||||||
|
new int[][]{
|
||||||
|
new int[]{android.R.attr.state_pressed},
|
||||||
|
new int[]{android.R.attr.state_selected},
|
||||||
|
new int[]{},
|
||||||
|
},
|
||||||
|
new int[]{0xFFFFFFFF, 0xFFFFFFFF, 0xFF202020});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final int NORMAL_COLOR = 0xFFC8C8C8;
|
||||||
|
protected static final int HOVERED_COLOR = 0xFFAEC2E8;
|
||||||
|
protected static final int PRESSED_COLOR = 0xFF3355AA;
|
||||||
|
|
||||||
protected final Activity activity;
|
protected final Activity activity;
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
private final int height;
|
||||||
private final View root;
|
private final View root;
|
||||||
|
|
||||||
|
// Views registered via registerHoverable() (every button, from both
|
||||||
|
// buildContent() and any later-added content) - hit-tested against the
|
||||||
|
// cursor position on every updateCursor() call so aiming at (not
|
||||||
|
// clicking) one visibly highlights it via android.R.attr.state_hovered
|
||||||
|
// (a real View state since API 14, paired with buttonBackground()
|
||||||
|
// above).
|
||||||
|
private final List<View> hoverableViews = new ArrayList<>();
|
||||||
|
private View hoveredView = null;
|
||||||
|
|
||||||
// contentBitmap holds just the panel's own rendered content (no cursor),
|
// contentBitmap holds just the panel's own rendered content (no cursor),
|
||||||
// redrawn only when that content actually changes (redrawContent(), via
|
// redrawn only when that content actually changes (redrawContent(), via
|
||||||
// handleTouch() or a subclass's own key handling) via root.draw() - a
|
// handleTouch() or a subclass's own key handling) via root.draw() - a
|
||||||
@@ -183,9 +238,95 @@ abstract class OverlayPanel {
|
|||||||
cursorVisible = visible;
|
cursorVisible = visible;
|
||||||
cursorX = x;
|
cursorX = x;
|
||||||
cursorY = y;
|
cursorY = y;
|
||||||
|
if (updateHoverState(x, y, visible)) {
|
||||||
|
// redrawContent() (called inside updateHoverState()) already
|
||||||
|
// composited and published this frame's pixels - nothing more
|
||||||
|
// to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
compositeAndPublish();
|
compositeAndPublish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Registers a button (or any View) to be hit-tested against the cursor
|
||||||
|
// position - called once per button, alongside buttonBackground()/
|
||||||
|
// setOnTouchListener(), from buildContent() (or any later content
|
||||||
|
// rebuild).
|
||||||
|
protected final void registerHoverable(View view) {
|
||||||
|
hoverableViews.add(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears whatever's currently hovered without waiting for the cursor to
|
||||||
|
// actually move off of it - call after hiding/showing content (e.g. a
|
||||||
|
// tab switch), since a view that was hovered right before becoming
|
||||||
|
// GONE would otherwise still be internally tracked as hovered (and
|
||||||
|
// wrongly show that way again) if it's later made VISIBLE again before
|
||||||
|
// the cursor position next changes.
|
||||||
|
protected final void resetHover() {
|
||||||
|
if (hoveredView != null) {
|
||||||
|
hoveredView.setHovered(false);
|
||||||
|
hoveredView = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute bounds of view within this panel's own coordinate space
|
||||||
|
// (i.e. relative to root, which is always laid out at (0,0,width,
|
||||||
|
// height) - see the constructor) - View's own getLeft()/getTop() are
|
||||||
|
// parent-relative, so this walks up the parent chain summing them,
|
||||||
|
// additionally subtracting each ancestor's own getScrollX()/getScrollY()
|
||||||
|
// (0 for a plain ViewGroup, non-zero for e.g. a ScrollView - see
|
||||||
|
// MenuOverlay's Controller Options tab) so a view inside a scrolled
|
||||||
|
// container reports its actual on-screen position, not its laid-out one.
|
||||||
|
// Assumes view is actually part of this panel's tree (every registered
|
||||||
|
// view always is, since registerHoverable() is only ever called from
|
||||||
|
// buildContent()).
|
||||||
|
private Rect absoluteBounds(View view) {
|
||||||
|
int left = 0;
|
||||||
|
int top = 0;
|
||||||
|
View current = view;
|
||||||
|
while (true) {
|
||||||
|
left += current.getLeft();
|
||||||
|
top += current.getTop();
|
||||||
|
if (current == root || !(current.getParent() instanceof View)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
View parent = (View) current.getParent();
|
||||||
|
left -= parent.getScrollX();
|
||||||
|
top -= parent.getScrollY();
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
return new Rect(left, top, left + view.getWidth(), top + view.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the hovered view changed (and so redrawContent() -
|
||||||
|
// which always publishes too - already ran); false if there's nothing
|
||||||
|
// more for updateCursor() to do beyond its own compositeAndPublish().
|
||||||
|
private boolean updateHoverState(float x, float y, boolean visible) {
|
||||||
|
View newHovered = null;
|
||||||
|
if (visible) {
|
||||||
|
for (View candidate : hoverableViews) {
|
||||||
|
if (candidate.getVisibility() != View.VISIBLE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (absoluteBounds(candidate).contains((int) x, (int) y)) {
|
||||||
|
newHovered = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newHovered == hoveredView) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (hoveredView != null) {
|
||||||
|
hoveredView.setHovered(false);
|
||||||
|
}
|
||||||
|
if (newHovered != null) {
|
||||||
|
newHovered.setHovered(true);
|
||||||
|
}
|
||||||
|
hoveredView = newHovered;
|
||||||
|
redrawContent();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void handleTouch(float u, float v, boolean down) {
|
private void handleTouch(float u, float v, boolean down) {
|
||||||
float x = u * width;
|
float x = u * width;
|
||||||
float y = v * height;
|
float y = v * height;
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import android.content.Context;
|
|||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
@@ -41,7 +39,8 @@ import org.libsdl.app.SDLSurface;
|
|||||||
public class QuestShockActivity extends SDLActivity {
|
public class QuestShockActivity extends SDLActivity {
|
||||||
private static final String TAG = "QuestShock";
|
private static final String TAG = "QuestShock";
|
||||||
private static final int PERMISSION_REQUEST_STORAGE = 1;
|
private static final int PERMISSION_REQUEST_STORAGE = 1;
|
||||||
private static final String GAME_DIR = "/sdcard/questshock";
|
// Package-visible: MenuOverlay reads controller_profiles/ from here.
|
||||||
|
static final String GAME_DIR = "/sdcard/questshock";
|
||||||
|
|
||||||
// Loaded here (rather than waiting for SDLActivity's own
|
// Loaded here (rather than waiting for SDLActivity's own
|
||||||
// getLibraries()/loadLibraries(), which only runs from super.onCreate())
|
// getLibraries()/loadLibraries(), which only runs from super.onCreate())
|
||||||
@@ -145,11 +144,25 @@ public class QuestShockActivity extends SDLActivity {
|
|||||||
// is already there, assume a previous run already extracted everything.
|
// is already there, assume a previous run already extracted everything.
|
||||||
File marker = new File(gameDir, "GET_ASSETS_QUEST.txt");
|
File marker = new File(gameDir, "GET_ASSETS_QUEST.txt");
|
||||||
if (!marker.exists()) {
|
if (!marker.exists()) {
|
||||||
copyAssetDir("shaders", new File(gameDir, "shaders"));
|
copyAssetDir("shaders", new File(gameDir, "shaders"), false);
|
||||||
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"));
|
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"), false);
|
||||||
copyAssetFile("GET_ASSETS_QUEST.txt", marker);
|
copyAssetFile("GET_ASSETS_QUEST.txt", marker, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refreshed on every launch (unlike the marker-gated block above) so
|
||||||
|
// it always matches the installed build - qs_config.c reads this as
|
||||||
|
// the initial config only when questshock.config.json doesn't exist
|
||||||
|
// yet, never overwriting a user's own saved config.
|
||||||
|
copyAssetFile("questshock.config.default.json",
|
||||||
|
new File(gameDir, "questshock.config.default.json"), true);
|
||||||
|
|
||||||
|
// Refreshed on every launch. MenuOverlay's Controller Options tab
|
||||||
|
// matches the active OpenXR interaction profile against these files'
|
||||||
|
// "interaction_profile" and shows the paired image (see CONFIG.md).
|
||||||
|
// Only the bundled filenames are overwritten, so profiles a user adds
|
||||||
|
// to this folder are kept.
|
||||||
|
copyAssetDir("controller_profiles", new File(gameDir, "controller_profiles"), true);
|
||||||
|
|
||||||
// Shockolate's engine/ is never patched to check this itself (see the
|
// Shockolate's engine/ is never patched to check this itself (see the
|
||||||
// class doc above) - it just does plain fopen("res/data/...", ...)
|
// class doc above) - it just does plain fopen("res/data/...", ...)
|
||||||
// and, on a fresh install with no game data copied in yet, that
|
// and, on a fresh install with no game data copied in yet, that
|
||||||
@@ -213,8 +226,8 @@ public class QuestShockActivity extends SDLActivity {
|
|||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyAssetFile(String assetPath, File dest) {
|
private void copyAssetFile(String assetPath, File dest, boolean overwrite) {
|
||||||
if (dest.exists()) {
|
if (dest.exists() && !overwrite) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
File parent = dest.getParentFile();
|
File parent = dest.getParentFile();
|
||||||
@@ -234,7 +247,7 @@ public class QuestShockActivity extends SDLActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyAssetDir(String assetDir, File destDir) {
|
private void copyAssetDir(String assetDir, File destDir, boolean overwrite) {
|
||||||
AssetManager assets = getAssets();
|
AssetManager assets = getAssets();
|
||||||
String[] entries;
|
String[] entries;
|
||||||
try {
|
try {
|
||||||
@@ -248,7 +261,7 @@ public class QuestShockActivity extends SDLActivity {
|
|||||||
}
|
}
|
||||||
destDir.mkdirs();
|
destDir.mkdirs();
|
||||||
for (String entry : entries) {
|
for (String entry : entries) {
|
||||||
copyAssetFile(assetDir + "/" + entry, new File(destDir, entry));
|
copyAssetFile(assetDir + "/" + entry, new File(destDir, entry), overwrite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package de.ladkau.questshock;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort (not professionally reviewed) translations for this app's own
|
||||||
|
* UI chrome text - the menu/keyboard panels this project draws itself, not
|
||||||
|
* the game's own resource-string text (which already has its own separate
|
||||||
|
* English/French/German data files). Selected by gShockPrefs.goLanguage (0
|
||||||
|
* English/1 French/2 German, Prefs.h) via MenuOverlay's native language
|
||||||
|
* query (qs_config_current_language()) - see MenuOverlay's periodic header-
|
||||||
|
* refresh timer, which also pushes it to KeyboardOverlay.
|
||||||
|
*
|
||||||
|
* Deliberately excludes the keyboard's own alphanumeric key labels (a-z,
|
||||||
|
* Tab, Space, Esc, ...) - those represent physical key positions/characters
|
||||||
|
* rather than translatable UI copy - and the restart-confirmation dialog's
|
||||||
|
* message text, which is built in native code (qs_config.c's
|
||||||
|
* qs_show_restart_dialog()) with an interpolated option-name list and is
|
||||||
|
* not currently reachable in practice (see CONFIG.md - neither Video
|
||||||
|
* Options option is flagged restart-required today).
|
||||||
|
*/
|
||||||
|
final class Strings {
|
||||||
|
static final int ENGLISH = 0;
|
||||||
|
static final int FRENCH = 1;
|
||||||
|
static final int GERMAN = 2;
|
||||||
|
|
||||||
|
private static final Map<String, String[]> TABLE = new HashMap<>();
|
||||||
|
|
||||||
|
private static void put(String key, String en, String fr, String de) {
|
||||||
|
TABLE.put(key, new String[]{en, fr, de});
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
put("keyboard", "Keyboard", "Clavier", "Tastatur");
|
||||||
|
put("save_configuration", "Save Configuration", "Enregistrer la configuration",
|
||||||
|
"Konfiguration speichern");
|
||||||
|
put("load_configuration", "Load Configuration", "Charger la configuration",
|
||||||
|
"Konfiguration laden");
|
||||||
|
put("reset_position", "Reset Position", "Réinitialiser la position",
|
||||||
|
"Position zurücksetzen");
|
||||||
|
put("main_menu", "Main Menu", "Menu principal", "Hauptmenü");
|
||||||
|
put("video_options", "Video Options", "Options vidéo", "Videooptionen");
|
||||||
|
put("dismiss", "Dismiss", "Fermer", "Schließen");
|
||||||
|
put("restart_now", "Restart Now", "Redémarrer maintenant", "Jetzt neu starten");
|
||||||
|
put("renderer", "Renderer", "Moteur de rendu", "Renderer");
|
||||||
|
put("resolution", "Resolution", "Résolution", "Auflösung");
|
||||||
|
put("software", "Software", "Logiciel", "Software");
|
||||||
|
put("language", "Language", "Langue", "Sprache");
|
||||||
|
put("english", "English", "Anglais", "Englisch");
|
||||||
|
put("french", "French", "Français", "Französisch");
|
||||||
|
put("german", "German", "Allemand", "Deutsch");
|
||||||
|
put("initial_screen", "Initial Screen", "Écran initial", "Startbildschirm");
|
||||||
|
put("game_screen", "Game Screen", "Écran de jeu", "Spielbildschirm");
|
||||||
|
put("width", "Width", "Largeur", "Breite");
|
||||||
|
put("distance", "Distance", "Distance", "Abstand");
|
||||||
|
// "OpenGL" is a proper/brand name - not translated, same in every
|
||||||
|
// language, so it has no entry here; call sites just use it as a
|
||||||
|
// literal.
|
||||||
|
put("keyboard_drag_title", "Keyboard - drag here to move",
|
||||||
|
"Clavier - faites glisser ici pour déplacer",
|
||||||
|
"Tastatur - hier ziehen zum Verschieben");
|
||||||
|
put("close", "Close", "Fermer", "Schließen");
|
||||||
|
|
||||||
|
// Controller Options tab: action row labels, keyed by xr_input.c's
|
||||||
|
// action names (see MenuOverlay.ACTION_KEYS).
|
||||||
|
put("controller_options", "Controller Options", "Options de manette", "Controller-Optionen");
|
||||||
|
put("action_move_forward", "Move Forward", "Avancer", "Vorwärts gehen");
|
||||||
|
put("action_move_back", "Move Back", "Reculer", "Rückwärts gehen");
|
||||||
|
put("action_strafe_left", "Strafe Left", "Pas latéral gauche", "Seitwärts links");
|
||||||
|
put("action_strafe_right", "Strafe Right", "Pas latéral droit", "Seitwärts rechts");
|
||||||
|
put("action_turn_left", "Turn Left", "Tourner à gauche", "Nach links drehen");
|
||||||
|
put("action_turn_right", "Turn Right", "Tourner à droite", "Nach rechts drehen");
|
||||||
|
put("action_jump", "Jump", "Sauter", "Springen");
|
||||||
|
put("action_mouse_click", "Click", "Cliquer", "Klicken");
|
||||||
|
put("action_mouse_double_click", "Double-Click", "Double-clic", "Doppelklick");
|
||||||
|
|
||||||
|
// Hotkey actions (see xr_input.c); all unbound by default.
|
||||||
|
put("action_stand", "Stand", "Debout", "Stehen");
|
||||||
|
put("action_crouch", "Crouch", "Accroupi", "Ducken");
|
||||||
|
put("action_prone", "Prone", "Couché", "Liegen");
|
||||||
|
put("action_pause", "Pause Game", "Mettre en pause", "Spiel pausieren");
|
||||||
|
put("action_full_view", "Fullscreen View", "Vue plein écran", "Vollbildansicht");
|
||||||
|
put("action_normal_view", "Normal View", "Vue normale", "Normale Ansicht");
|
||||||
|
put("action_map_view", "Automap View", "Vue de la carte", "Automap-Ansicht");
|
||||||
|
put("action_save_game", "Save Game", "Sauvegarder", "Spiel speichern");
|
||||||
|
put("action_load_game", "Load Game", "Charger", "Spiel laden");
|
||||||
|
put("action_select_grenade", "Select Grenade", "Sélectionner une grenade",
|
||||||
|
"Granate auswählen");
|
||||||
|
put("action_arm_grenade", "Arm Grenade", "Armer la grenade", "Granate scharfmachen");
|
||||||
|
put("action_select_drug", "Select Drug", "Sélectionner une drogue", "Droge auswählen");
|
||||||
|
put("action_use_drug", "Use Drug", "Utiliser la drogue", "Droge benutzen");
|
||||||
|
put("action_toggle_online_help", "Toggle Online Help", "Basculer l'aide en ligne",
|
||||||
|
"Online-Hilfe umschalten");
|
||||||
|
put("action_show_help", "Show Help", "Afficher l'aide", "Hilfe anzeigen");
|
||||||
|
put("action_toggle_music", "Toggle Music", "Basculer la musique", "Musik umschalten");
|
||||||
|
put("action_quit", "Quit Game", "Quitter le jeu", "Spiel beenden");
|
||||||
|
put("action_cycle_weapon_next", "Next Weapon", "Arme suivante", "Nächste Waffe");
|
||||||
|
put("action_cycle_weapon_prev", "Previous Weapon", "Arme précédente", "Vorherige Waffe");
|
||||||
|
put("action_cycle_detail", "Cycle Detail Level", "Changer le niveau de détail",
|
||||||
|
"Detailgrad wechseln");
|
||||||
|
put("action_reload_weapon_primary", "Reload Weapon (Primary)",
|
||||||
|
"Recharger l'arme (principal)", "Waffe nachladen (primär)");
|
||||||
|
put("action_reload_weapon_secondary", "Reload Weapon (Secondary)",
|
||||||
|
"Recharger l'arme (secondaire)", "Waffe nachladen (sekundär)");
|
||||||
|
put("action_hud_color", "Cycle HUD Color", "Changer la couleur de l'interface",
|
||||||
|
"HUD-Farbe wechseln");
|
||||||
|
put("action_clear_fullscreen", "Clear Fullscreen View", "Quitter la vue plein écran",
|
||||||
|
"Vollbildansicht schließen");
|
||||||
|
put("action_mfd_left_1", "Left MFD: Weapon", "MFD gauche : arme", "Linkes MFD: Waffe");
|
||||||
|
put("action_mfd_left_2", "Left MFD: Item", "MFD gauche : objet", "Linkes MFD: Gegenstand");
|
||||||
|
put("action_mfd_left_3", "Left MFD: Map", "MFD gauche : carte", "Linkes MFD: Karte");
|
||||||
|
put("action_mfd_left_4", "Left MFD: Target", "MFD gauche : cible", "Linkes MFD: Ziel");
|
||||||
|
put("action_mfd_left_5", "Left MFD: Info", "MFD gauche : info", "Linkes MFD: Info");
|
||||||
|
put("action_mfd_right_1", "Right MFD: Weapon", "MFD droit : arme", "Rechtes MFD: Waffe");
|
||||||
|
put("action_mfd_right_2", "Right MFD: Item", "MFD droit : objet",
|
||||||
|
"Rechtes MFD: Gegenstand");
|
||||||
|
put("action_mfd_right_3", "Right MFD: Map", "MFD droit : carte", "Rechtes MFD: Karte");
|
||||||
|
put("action_mfd_right_4", "Right MFD: Target", "MFD droit : cible", "Rechtes MFD: Ziel");
|
||||||
|
put("action_mfd_right_5", "Right MFD: Info", "MFD droit : info", "Rechtes MFD: Info");
|
||||||
|
put("action_fire_weapon", "Fire Weapon", "Tirer", "Waffe abfeuern");
|
||||||
|
put("action_free_left_thumbstick_view", "Free View (Left Stick)",
|
||||||
|
"Vue libre (stick gauche)", "Freie Ansicht (linker Stick)");
|
||||||
|
put("action_free_right_thumbstick_view", "Free View (Right Stick)",
|
||||||
|
"Vue libre (stick droit)", "Freie Ansicht (rechter Stick)");
|
||||||
|
|
||||||
|
// Cyberspace actions (active only in cyberspace, see xr_input.c).
|
||||||
|
put("action_cyber_thrust", "Cyberspace: Thrust", "Cyberespace : poussée",
|
||||||
|
"Cyberspace: Schub");
|
||||||
|
put("action_cyber_climb", "Cyberspace: Climb", "Cyberespace : monter",
|
||||||
|
"Cyberspace: Steigen");
|
||||||
|
put("action_cyber_dive", "Cyberspace: Dive", "Cyberespace : plonger",
|
||||||
|
"Cyberspace: Sinken");
|
||||||
|
put("action_cyber_bank_left", "Cyberspace: Bank Left", "Cyberespace : virage à gauche",
|
||||||
|
"Cyberspace: Links kippen");
|
||||||
|
put("action_cyber_bank_right", "Cyberspace: Bank Right", "Cyberespace : virage à droite",
|
||||||
|
"Cyberspace: Rechts kippen");
|
||||||
|
put("action_cyber_roll_left", "Cyberspace: Roll Left", "Cyberespace : roulis gauche",
|
||||||
|
"Cyberspace: Links rollen");
|
||||||
|
put("action_cyber_roll_right", "Cyberspace: Roll Right", "Cyberespace : roulis droit",
|
||||||
|
"Cyberspace: Rechts rollen");
|
||||||
|
put("action_cyber_climb_left", "Cyberspace: Climb Left", "Cyberespace : monter à gauche",
|
||||||
|
"Cyberspace: Links steigen");
|
||||||
|
put("action_cyber_climb_right", "Cyberspace: Climb Right", "Cyberespace : monter à droite",
|
||||||
|
"Cyberspace: Rechts steigen");
|
||||||
|
put("action_cyber_dive_left", "Cyberspace: Dive Left", "Cyberespace : plonger à gauche",
|
||||||
|
"Cyberspace: Links sinken");
|
||||||
|
put("action_cyber_dive_right", "Cyberspace: Dive Right", "Cyberespace : plonger à droite",
|
||||||
|
"Cyberspace: Rechts sinken");
|
||||||
|
put("capture_prompt", "Press a button…", "Appuyez sur un bouton…", "Taste drücken…");
|
||||||
|
put("reset_to_defaults", "Reset to Defaults", "Réinitialiser", "Auf Standard zurücksetzen");
|
||||||
|
put("generic_profile_note",
|
||||||
|
"This controller only exposes a trigger and a menu button - most actions above have "
|
||||||
|
+ "nothing to bind to.",
|
||||||
|
"Cette manette n'expose qu'une gâchette et un bouton menu - la plupart des actions "
|
||||||
|
+ "ci-dessus n'ont rien à quoi être assignées.",
|
||||||
|
"Dieser Controller bietet nur einen Trigger und eine Menütaste - die meisten "
|
||||||
|
+ "Aktionen oben haben nichts, woran sie gebunden werden können.");
|
||||||
|
|
||||||
|
// Raw physical-input display names (xr_input.c's kRawInputNames);
|
||||||
|
// "none" is the unbound sentinel.
|
||||||
|
put("raw_left_trigger", "Left Trigger", "Gâchette gauche", "Linker Trigger");
|
||||||
|
put("raw_right_trigger", "Right Trigger", "Gâchette droite", "Rechter Trigger");
|
||||||
|
put("raw_left_squeeze", "Left Grip", "Préhension gauche", "Linker Griff");
|
||||||
|
put("raw_right_squeeze", "Right Grip", "Préhension droite", "Rechter Griff");
|
||||||
|
put("raw_left_x", "X Button", "Bouton X", "X-Taste");
|
||||||
|
put("raw_left_y", "Y Button", "Bouton Y", "Y-Taste");
|
||||||
|
put("raw_right_a", "A Button", "Bouton A", "A-Taste");
|
||||||
|
put("raw_right_b", "B Button", "Bouton B", "B-Taste");
|
||||||
|
put("raw_left_thumbstick_click", "Left Stick Click", "Clic du stick gauche",
|
||||||
|
"Linker Stick-Klick");
|
||||||
|
put("raw_right_thumbstick_click", "Right Stick Click", "Clic du stick droit",
|
||||||
|
"Rechter Stick-Klick");
|
||||||
|
put("raw_left_thumbstick_up", "Left Stick Up", "Stick gauche haut", "Linker Stick hoch");
|
||||||
|
put("raw_left_thumbstick_down", "Left Stick Down", "Stick gauche bas", "Linker Stick runter");
|
||||||
|
put("raw_left_thumbstick_left", "Left Stick Left", "Stick gauche à gauche",
|
||||||
|
"Linker Stick links");
|
||||||
|
put("raw_left_thumbstick_right", "Left Stick Right", "Stick gauche à droite",
|
||||||
|
"Linker Stick rechts");
|
||||||
|
put("raw_right_thumbstick_up", "Right Stick Up", "Stick droit haut", "Rechter Stick hoch");
|
||||||
|
put("raw_right_thumbstick_down", "Right Stick Down", "Stick droit bas",
|
||||||
|
"Rechter Stick runter");
|
||||||
|
put("raw_right_thumbstick_left", "Right Stick Left", "Stick droit à gauche",
|
||||||
|
"Rechter Stick links");
|
||||||
|
put("raw_right_thumbstick_right", "Right Stick Right", "Stick droit à droite",
|
||||||
|
"Rechter Stick rechts");
|
||||||
|
put("raw_trigger", "Either Trigger", "L'une ou l'autre gâchette", "Beliebiger Trigger");
|
||||||
|
put("raw_squeeze", "Either Grip", "L'une ou l'autre préhension", "Beliebiger Griff");
|
||||||
|
put("raw_none", "(unbound)", "(non assigné)", "(nicht zugewiesen)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Strings() {}
|
||||||
|
|
||||||
|
static String get(String key, int language) {
|
||||||
|
String[] variants = TABLE.get(key);
|
||||||
|
if (variants == null) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
if (language < 0 || language >= variants.length) {
|
||||||
|
language = ENGLISH;
|
||||||
|
}
|
||||||
|
return variants[language];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,18 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.1.1'
|
// 8.5.1+ needed for AGP to automatically 16 KB-align native
|
||||||
|
// libraries' zip entries within the APK - the actual .so's are
|
||||||
|
// already ELF-segment-aligned for this regardless of AGP version
|
||||||
|
// (see build-image/Dockerfile's ANDROID_16KB_LDFLAGS and
|
||||||
|
// app/build.gradle's own CMAKE_SHARED_LINKER_FLAGS), but that alone
|
||||||
|
// doesn't get them 16 KB-aligned *within the APK's zip*, which is
|
||||||
|
// what Android Studio's "not compatible with 16 KB devices"
|
||||||
|
// warning is actually about. Needs Gradle 8.7+ (see
|
||||||
|
// gradle/wrapper/gradle-wrapper.properties) and JDK 17 in whatever
|
||||||
|
// runs gradlew (already the case - see build-image/Dockerfile's
|
||||||
|
// openjdk-17-jdk-headless).
|
||||||
|
classpath 'com.android.tools.build:gradle:8.5.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
--- a/src/MacSrc/Shock.c
|
--- a/src/MacSrc/Shock.c
|
||||||
+++ b/src/MacSrc/Shock.c
|
+++ b/src/MacSrc/Shock.c
|
||||||
@@ -162,6 +162,17 @@
|
@@ -162,9 +162,26 @@
|
||||||
void InitSDL() {
|
void InitSDL() {
|
||||||
SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
|
SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
|
||||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
|
||||||
@@ -12,9 +12,18 @@
|
|||||||
+ // src/audio/aaudio/SDL_aaudio.c). SDLSound.c opens two: one directly via
|
+ // src/audio/aaudio/SDL_aaudio.c). SDLSound.c opens two: one directly via
|
||||||
+ // SDL_OpenAudioDevice() for cutscene audio, one via Mix_OpenAudio() for
|
+ // SDL_OpenAudioDevice() for cutscene audio, one via Mix_OpenAudio() for
|
||||||
+ // SFX/MIDI. The older OpenSL ES backend has no such limitation, so force
|
+ // SFX/MIDI. The older OpenSL ES backend has no such limitation, so force
|
||||||
+ // it instead of AAudio.
|
+ // it instead of AAudio. Uses SDL_HINT_OVERRIDE priority so nothing set
|
||||||
+ SDL_SetHint(SDL_HINT_AUDIODRIVER, "openslES");
|
+ // earlier (or by SDL/the platform) can take precedence over it.
|
||||||
|
+ SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, "openslES", SDL_HINT_OVERRIDE);
|
||||||
+#endif
|
+#endif
|
||||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) < 0) {
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) < 0) {
|
||||||
DEBUG("%s: Init failed", __FUNCTION__);
|
DEBUG("%s: Init failed", __FUNCTION__);
|
||||||
}
|
}
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // Standing diagnostic: reports which audio driver SDL actually selected,
|
||||||
|
+ // to confirm the openslES override above took effect on-device.
|
||||||
|
+ INFO("SDL audio driver: %s", SDL_GetCurrentAudioDriver());
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
// TODO: figure out some universal set of settings that work...
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
--- a/src/MacSrc/ShockBitmap.c
|
--- a/src/MacSrc/ShockBitmap.c
|
||||||
+++ b/src/MacSrc/ShockBitmap.c
|
+++ b/src/MacSrc/ShockBitmap.c
|
||||||
@@ -42,11 +42,26 @@
|
@@ -27,6 +27,9 @@
|
||||||
|
#include "Shock.h"
|
||||||
|
#include "ShockBitmap.h"
|
||||||
|
#include "2d.h"
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+#include "OpenGL.h"
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
//--------------------
|
||||||
|
// Globals
|
||||||
|
@@ -42,11 +45,26 @@
|
||||||
|
|
||||||
SDL_RenderClear(renderer);
|
SDL_RenderClear(renderer);
|
||||||
|
|
||||||
@@ -27,3 +37,32 @@
|
|||||||
|
|
||||||
SDL_RenderSetLogicalSize(renderer, width, height);
|
SDL_RenderSetLogicalSize(renderer, width, height);
|
||||||
|
|
||||||
|
@@ -54,6 +72,28 @@
|
||||||
|
|
||||||
|
gScreenWide = width;
|
||||||
|
gScreenHigh = height;
|
||||||
|
+
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // Skipping SDL_SetWindowSize() above (see the comment for why) also
|
||||||
|
+ // means no native resize event ever fires on Android when the in-game
|
||||||
|
+ // resolution changes - opengl_resize() (OpenGL.cc) only ever runs from
|
||||||
|
+ // that event (see sdl_events.c) or once at OpenGL init, and it's what
|
||||||
|
+ // (re)creates backupBuffer, the offscreen 3D-view render target, sized
|
||||||
|
+ // to the logical resolution just changed above. Without this, it stays
|
||||||
|
+ // stuck at whatever size it was at launch while the rest of the game
|
||||||
|
+ // (drawSurface/gScreenWide/gScreenHigh, just updated above) moves on to
|
||||||
|
+ // the new resolution - the actual cause of the display breaking and
|
||||||
|
+ // becoming unresponsive to further input after changing resolution
|
||||||
|
+ // in-game. Call it directly instead - after gScreenWide/gScreenHigh
|
||||||
|
+ // update above, same as the desktop resize event (delivered via
|
||||||
|
+ // SDL_PollEvent(), so always processed after this function has already
|
||||||
|
+ // returned) - using the same physical window size init_opengl() itself
|
||||||
|
+ // used (SDL_GetWindowSize()); that never changes on Android, only the
|
||||||
|
+ // logical size set above does.
|
||||||
|
+ int physWidth, physHeight;
|
||||||
|
+ SDL_GetWindowSize(window, &physWidth, &physHeight);
|
||||||
|
+ opengl_resize(physWidth, physHeight);
|
||||||
|
+#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
--- a/src/MacSrc/SDLSound.c
|
||||||
|
+++ b/src/MacSrc/SDLSound.c
|
||||||
|
@@ -42,7 +42,7 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Mix_OpenAudio(48000, AUDIO_S16SYS, 2, 2048) < 0) {
|
||||||
|
- ERROR("%s: Couldn't open audio device", __FUNCTION__);
|
||||||
|
+ ERROR("%s: Couldn't open audio device: %s", __FUNCTION__, SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
Mix_AllocateChannels(SND_MAX_SAMPLES);
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
--- a/src/MacSrc/SDLSound.c
|
||||||
|
+++ b/src/MacSrc/SDLSound.c
|
||||||
|
@@ -19,24 +19,6 @@
|
||||||
|
|
||||||
|
// Startup the sound system
|
||||||
|
|
||||||
|
- SDL_AudioSpec spec, obtained;
|
||||||
|
- spec.freq = 48000;
|
||||||
|
- spec.format = AUDIO_S16SYS;
|
||||||
|
- spec.channels = 2;
|
||||||
|
- spec.samples = 2048;
|
||||||
|
- spec.callback = AudioStreamCallback;
|
||||||
|
- spec.userdata = (void *)&cutscene_audiostream;
|
||||||
|
-
|
||||||
|
- extern SDL_AudioDeviceID device;
|
||||||
|
- device = SDL_OpenAudioDevice(NULL, 0, &spec, &obtained, 0);
|
||||||
|
-
|
||||||
|
- if (device == 0) {
|
||||||
|
- ERROR("Could not open SDL audio: %s", SDL_GetError());
|
||||||
|
- } else {
|
||||||
|
- INFO("Opened Music Stream, deviceID %d, freq %d, size %d, format %d, channels %d, samples %d", device,
|
||||||
|
- obtained.freq, obtained.size, obtained.format, obtained.channels, obtained.samples);
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
if (Mix_Init(MIX_INIT_MP3) < 0) {
|
||||||
|
ERROR("%s: Init failed", __FUNCTION__);
|
||||||
|
}
|
||||||
|
@@ -50,10 +32,18 @@
|
||||||
|
Mix_HookMusic(MusicCallback, (void *)&MusicDev);
|
||||||
|
Mix_VolumeMusic(MIX_MAX_VOLUME); // use max volume for music stream
|
||||||
|
|
||||||
|
+ // Cutscene/audiolog playback (AudioStreamCallback, see cutsloop.c) used to
|
||||||
|
+ // run through its own SDL_OpenAudioDevice() device, separate from this
|
||||||
|
+ // one - SDL only ever allows one open non-capture device on Android's
|
||||||
|
+ // audio backends, so that second open always failed with "Audio device
|
||||||
|
+ // already open" and both SFX and MIDI music (both routed through this
|
||||||
|
+ // Mix_OpenAudio device) silently never played. Mixed into this device's
|
||||||
|
+ // post-mix stage instead, so there is only ever one device open.
|
||||||
|
+ Mix_SetPostMix(AudioStreamCallback, (void *)&cutscene_audiostream);
|
||||||
|
+
|
||||||
|
InitReadXMI();
|
||||||
|
|
||||||
|
atexit(Mix_CloseAudio);
|
||||||
|
- atexit(SDL_CloseAudio);
|
||||||
|
|
||||||
|
return OK;
|
||||||
|
}
|
||||||
|
--- a/src/MacSrc/Shock.c
|
||||||
|
+++ b/src/MacSrc/Shock.c
|
||||||
|
@@ -60,8 +60,6 @@
|
||||||
|
SDL_Palette *sdlPalette;
|
||||||
|
SDL_Renderer *renderer;
|
||||||
|
|
||||||
|
-SDL_AudioDeviceID device;
|
||||||
|
-
|
||||||
|
int num_args;
|
||||||
|
char **arg_values;
|
||||||
|
|
||||||
|
--- a/src/GameSrc/cutsloop.c
|
||||||
|
+++ b/src/GameSrc/cutsloop.c
|
||||||
|
@@ -83,15 +83,25 @@
|
||||||
|
extern char FrnSubtitle[256];
|
||||||
|
extern char GerSubtitle[256];
|
||||||
|
|
||||||
|
-extern SDL_AudioDeviceID device;
|
||||||
|
-
|
||||||
|
-
|
||||||
|
+// Runs as SDL_mixer's post-mix hook (see snd_start_digital() in
|
||||||
|
+// SDLSound.c) rather than a dedicated SDL audio device's callback, so
|
||||||
|
+// SFX/MIDI music sharing the same device are already mixed into `stream`
|
||||||
|
+// by the time this runs - mix in rather than overwrite, so they aren't
|
||||||
|
+// silenced whenever a cutscene or audiolog is playing.
|
||||||
|
void AudioStreamCallback(void *userdata, unsigned char *stream, int len)
|
||||||
|
{
|
||||||
|
+ static Uint8 mixbuf[MOVIE_DEFAULT_BLOCKLEN * 4];
|
||||||
|
SDL_AudioStream *as = *(SDL_AudioStream **)userdata;
|
||||||
|
|
||||||
|
- if (as != NULL && SDL_AudioStreamAvailable(as) > 0)
|
||||||
|
- SDL_AudioStreamGet(as, stream, len);
|
||||||
|
+ if (as == NULL || SDL_AudioStreamAvailable(as) <= 0)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (len > (int)sizeof(mixbuf))
|
||||||
|
+ len = (int)sizeof(mixbuf);
|
||||||
|
+
|
||||||
|
+ int got = SDL_AudioStreamGet(as, mixbuf, len);
|
||||||
|
+ if (got > 0)
|
||||||
|
+ SDL_MixAudioFormat(stream, mixbuf, AUDIO_S16SYS, got, SDL_MIX_MAXVOLUME);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -153,11 +163,13 @@
|
||||||
|
|
||||||
|
if (cutscene_audiostream != NULL)
|
||||||
|
{
|
||||||
|
- SDL_PauseAudioDevice(device, 1);
|
||||||
|
- SDL_Delay(1);
|
||||||
|
-
|
||||||
|
+ // AudioStreamCallback (SDL_mixer's post-mix hook) runs continuously on
|
||||||
|
+ // SDL_mixer's own audio thread and reads this pointer - lock around
|
||||||
|
+ // freeing it so that thread can't be mid-read when it's freed.
|
||||||
|
+ SDL_LockAudio();
|
||||||
|
SDL_FreeAudioStream(cutscene_audiostream);
|
||||||
|
cutscene_audiostream = NULL;
|
||||||
|
+ SDL_UnlockAudio();
|
||||||
|
|
||||||
|
if (cutscene_audiobuffer)
|
||||||
|
{
|
||||||
|
@@ -184,8 +196,6 @@
|
||||||
|
|
||||||
|
if (cutscene_audiostream)
|
||||||
|
{
|
||||||
|
- SDL_PauseAudioDevice(device, 0);
|
||||||
|
-
|
||||||
|
if (cutscene_audiobuffer_size > 0)
|
||||||
|
{
|
||||||
|
// === adjust volume in buffer here ===
|
||||||
|
@@ -325,10 +335,9 @@
|
||||||
|
|
||||||
|
if (sfx_on)
|
||||||
|
{
|
||||||
|
- SDL_PauseAudioDevice(device, 1);
|
||||||
|
- SDL_Delay(1);
|
||||||
|
-
|
||||||
|
+ SDL_LockAudio();
|
||||||
|
cutscene_audiostream = SDL_NewAudioStream(AUDIO_U8, 1, fix_int(amovie->a.sampleRate), AUDIO_S16SYS, 2, 48000);
|
||||||
|
+ SDL_UnlockAudio();
|
||||||
|
|
||||||
|
cutscene_audiobuffer_pos = cutscene_audiobuffer;
|
||||||
|
}
|
||||||
|
--- a/src/GameSrc/audiolog.c
|
||||||
|
+++ b/src/GameSrc/audiolog.c
|
||||||
|
@@ -61,8 +61,6 @@
|
||||||
|
extern uchar curr_alog_vol;
|
||||||
|
extern char which_lang;
|
||||||
|
|
||||||
|
-extern SDL_AudioDeviceID device;
|
||||||
|
-
|
||||||
|
errtype audiolog_init(void) { return OK; }
|
||||||
|
|
||||||
|
errtype audiolog_play(int email_id) {
|
||||||
|
@@ -114,10 +112,9 @@
|
||||||
|
|
||||||
|
DEBUG("%s: Playing email", __FUNCTION__);
|
||||||
|
|
||||||
|
- SDL_PauseAudioDevice(device, 1);
|
||||||
|
- SDL_Delay(1);
|
||||||
|
-
|
||||||
|
+ SDL_LockAudio();
|
||||||
|
cutscene_audiostream = SDL_NewAudioStream(AUDIO_U8, 1, fix_int(palog->a.sampleRate), AUDIO_S16SYS, 2, 48000);
|
||||||
|
+ SDL_UnlockAudio();
|
||||||
|
|
||||||
|
audiolog_audiobuffer_pos = audiolog_audiobuffer;
|
||||||
|
|
||||||
|
@@ -150,11 +147,14 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cutscene_audiostream != NULL) {
|
||||||
|
- SDL_PauseAudioDevice(device, 1);
|
||||||
|
- SDL_Delay(1);
|
||||||
|
-
|
||||||
|
+ // AudioStreamCallback (SDL_mixer's post-mix hook, see cutsloop.c)
|
||||||
|
+ // runs continuously on SDL_mixer's own audio thread and reads this
|
||||||
|
+ // pointer - lock around freeing it so that thread can't be
|
||||||
|
+ // mid-read when it's freed.
|
||||||
|
+ SDL_LockAudio();
|
||||||
|
SDL_FreeAudioStream(cutscene_audiostream);
|
||||||
|
cutscene_audiostream = NULL;
|
||||||
|
+ SDL_UnlockAudio();
|
||||||
|
|
||||||
|
if (audiolog_audiobuffer) {
|
||||||
|
free(audiolog_audiobuffer);
|
||||||
|
@@ -177,8 +177,6 @@
|
||||||
|
|
||||||
|
errtype audiolog_loop_callback(void) {
|
||||||
|
if (cutscene_audiostream) {
|
||||||
|
- SDL_PauseAudioDevice(device, 0);
|
||||||
|
-
|
||||||
|
if (audiolog_audiobuffer_size > 0) {
|
||||||
|
int i, vol = curr_alog_vol * 127 / 100; // convert from 0-100 to 0-127
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
--- a/src/MusicSrc/MusicDevice.c
|
||||||
|
+++ b/src/MusicSrc/MusicDevice.c
|
||||||
|
@@ -569,6 +569,14 @@
|
||||||
|
// connected
|
||||||
|
ndev->dev.isOpen = 1;
|
||||||
|
ndev->dev.outputIndex = outputIndex;
|
||||||
|
+#else
|
||||||
|
+ // No native MIDI backend implemented for this platform (e.g. Android) -
|
||||||
|
+ // report failure so InitDecXMI()'s caller falls back to another device
|
||||||
|
+ // instead of treating an unopened device as successfully initialized.
|
||||||
|
+ (void)outputIndex;
|
||||||
|
+ (void)samplerate;
|
||||||
|
+ WARN("NativeMidiInit(): native MIDI is not implemented on this platform");
|
||||||
|
+ return -1;
|
||||||
|
#endif
|
||||||
|
// suppress compiler warnings
|
||||||
|
(void)outputIndex;
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
--- a/src/MacSrc/OpenGL.cc
|
||||||
|
+++ b/src/MacSrc/OpenGL.cc
|
||||||
|
@@ -391,7 +391,16 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
void opengl_resize(int width, int height) {
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // Skipped on Android - see opengl_swap_and_restore()'s comment below for
|
||||||
|
+ // why re-asserting an already-current context/surface every call breaks
|
||||||
|
+ // gl4es's own "current framebuffer" bookkeeping, even though the real
|
||||||
|
+ // driver binding never actually changes. This turned out to be the
|
||||||
|
+ // actual source of the persistent per-frame GL_INVALID_OPERATION (1282)
|
||||||
|
+ // that plagued 3D rendering: this same unconditional call was present at
|
||||||
|
+ // the top of nearly every function in this file, not just here.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
int logical_width, logical_height;
|
||||||
|
SDL_RenderGetLogicalSize(renderer, &logical_width, &logical_height);
|
||||||
|
@@ -443,7 +452,10 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
void opengl_end_frame() {
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
// Done rendering to the frame buffer, reset back to normal
|
||||||
|
BindFrameBuffer(nullptr);
|
||||||
|
@@ -470,7 +482,10 @@
|
||||||
|
static bool nightsight_active() { return WareActive(player_struct.hardwarez_status[HARDWARE_GOGGLE_INFRARED]); }
|
||||||
|
|
||||||
|
void opengl_start_frame() {
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
// Start rendering to our frame buffer canvas
|
||||||
|
BindFrameBuffer(&backupBuffer);
|
||||||
|
@@ -500,6 +515,130 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
+// Real per-vertex arrays, not glBegin/glVertexAttrib/glVertex3f immediate
|
||||||
|
+// mode: glVertexAttrib*() is GLES2's *constant*-attribute API - without an
|
||||||
|
+// enabled vertex array it sets one value for the whole draw call, not a
|
||||||
|
+// fresh value per vertex. gl4es's immediate-mode emulation only reproduces
|
||||||
|
+// desktop GL's per-vertex-capture behavior for attribute 0 (position,
|
||||||
|
+// driven directly by glVertex3f), not for custom attributes like this
|
||||||
|
+// shader's texcoords/light. That means every one of this file's
|
||||||
|
+// glBegin(GL_TRIANGLE_STRIP)-based draws - not just the UI-overlay quad
|
||||||
|
+// this was first found on, but also opengl_light_tmap() (every textured
|
||||||
|
+// wall/floor polygon), opengl_bitmap() (sprites) and opengl_draw_poly()
|
||||||
|
+// (flat/gouraud polygons) via draw_vertex() - draws every vertex with
|
||||||
|
+// whatever texcoord/light was set for the *last* vertex instead of its
|
||||||
|
+// own, which is what actually produced flat-colored/untextured world
|
||||||
|
+// geometry after this patch's first revision only fixed the final
|
||||||
|
+// screen-composite quad (the one bug this was originally found on).
|
||||||
|
+//
|
||||||
|
+// android_imm_begin()/android_imm_vertex()/android_imm_end() below emulate
|
||||||
|
+// just enough of glBegin(GL_TRIANGLE_STRIP)/glVertexAttrib*()/glVertex3f()
|
||||||
|
+// immediate mode to replace every such call site on Android, by buffering
|
||||||
|
+// vertices and flushing them as one real glDrawArrays() call - the engine's
|
||||||
|
+// other rendering still uses real immediate mode for attribute 0 elsewhere
|
||||||
|
+// on desktop builds, which depends on it not having an enabled array bound,
|
||||||
|
+// so android_imm_end() always disables the arrays it enables before
|
||||||
|
+// returning.
|
||||||
|
+struct AndroidImmVertex {
|
||||||
|
+ float x, y, z;
|
||||||
|
+ float u, v;
|
||||||
|
+ float light;
|
||||||
|
+};
|
||||||
|
+
|
||||||
|
+// Arbitrarily-sized, but generous: the engine's polygons (room walls/
|
||||||
|
+// floors, sprites, screen-filling quads) are all small, hand-authored
|
||||||
|
+// shapes, never anywhere near this many vertices.
|
||||||
|
+static AndroidImmVertex s_immVerts[64];
|
||||||
|
+static int s_immVertCount;
|
||||||
|
+static GLint s_immTcAttrib;
|
||||||
|
+static GLint s_immLightAttrib;
|
||||||
|
+
|
||||||
|
+// tcAttrib < 0 means this draw has no texcoord attribute (flat/gouraud
|
||||||
|
+// polygons via opengl_draw_poly()) - matches draw_vertex()'s own
|
||||||
|
+// `tcAttrib >= 0` check below.
|
||||||
|
+static void android_imm_begin(GLint tcAttrib, GLint lightAttrib) {
|
||||||
|
+ s_immVertCount = 0;
|
||||||
|
+ s_immTcAttrib = tcAttrib;
|
||||||
|
+ s_immLightAttrib = lightAttrib;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static void android_imm_vertex(float x, float y, float z, float u, float v, float light) {
|
||||||
|
+ if (s_immVertCount >= (int)(sizeof(s_immVerts) / sizeof(s_immVerts[0]))) {
|
||||||
|
+ WARN("android_imm_vertex: dropping vertex past the %d-vertex limit", s_immVertCount);
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+ AndroidImmVertex &vert = s_immVerts[s_immVertCount++];
|
||||||
|
+ vert.x = x;
|
||||||
|
+ vert.y = y;
|
||||||
|
+ vert.z = z;
|
||||||
|
+ vert.u = u;
|
||||||
|
+ vert.v = v;
|
||||||
|
+ vert.light = light;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static void android_imm_end() {
|
||||||
|
+ if (s_immVertCount < 3)
|
||||||
|
+ return;
|
||||||
|
+ glEnableVertexAttribArray(0);
|
||||||
|
+ if (s_immTcAttrib >= 0)
|
||||||
|
+ glEnableVertexAttribArray(s_immTcAttrib);
|
||||||
|
+ glEnableVertexAttribArray(s_immLightAttrib);
|
||||||
|
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex), &s_immVerts[0].x);
|
||||||
|
+ if (s_immTcAttrib >= 0)
|
||||||
|
+ glVertexAttribPointer(s_immTcAttrib, 2, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex),
|
||||||
|
+ &s_immVerts[0].u);
|
||||||
|
+ glVertexAttribPointer(s_immLightAttrib, 1, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex),
|
||||||
|
+ &s_immVerts[0].light);
|
||||||
|
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, s_immVertCount);
|
||||||
|
+ glDisableVertexAttribArray(0);
|
||||||
|
+ if (s_immTcAttrib >= 0)
|
||||||
|
+ glDisableVertexAttribArray(s_immTcAttrib);
|
||||||
|
+ glDisableVertexAttribArray(s_immLightAttrib);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+// Same underlying bug as android_imm_*() above, for opengl_draw_star():
|
||||||
|
+// opengl_begin_stars()/opengl_draw_star()/opengl_end_stars() draw up to
|
||||||
|
+// NUM_STARS (rendtool.c) individual GL_POINTS between one begin/end pair,
|
||||||
|
+// each with its own per-star brightness via glVertexAttrib1f(lightAttrib,
|
||||||
|
+// ...)/glVertex3f() immediate mode - under gl4es that made every star on
|
||||||
|
+// screen render at whatever brightness was set for the *last* one. Shaped
|
||||||
|
+// differently enough from android_imm_*() above (GL_POINTS instead of
|
||||||
|
+// GL_TRIANGLE_STRIP, far more vertices, no texcoord) to warrant its own
|
||||||
|
+// small buffer rather than overloading that one.
|
||||||
|
+static AndroidImmVertex s_starVerts[1000]; // NUM_STARS, GameSrc/rendtool.c
|
||||||
|
+static int s_starVertCount;
|
||||||
|
+static GLint s_starLightAttrib;
|
||||||
|
+
|
||||||
|
+static void android_stars_begin(GLint lightAttrib) {
|
||||||
|
+ s_starVertCount = 0;
|
||||||
|
+ s_starLightAttrib = lightAttrib;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static void android_stars_vertex(float x, float y, float z, float light) {
|
||||||
|
+ if (s_starVertCount >= (int)(sizeof(s_starVerts) / sizeof(s_starVerts[0]))) {
|
||||||
|
+ WARN("android_stars_vertex: dropping star past the %d-star limit", s_starVertCount);
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+ AndroidImmVertex &vert = s_starVerts[s_starVertCount++];
|
||||||
|
+ vert.x = x;
|
||||||
|
+ vert.y = y;
|
||||||
|
+ vert.z = z;
|
||||||
|
+ vert.light = light;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static void android_stars_end() {
|
||||||
|
+ if (s_starVertCount == 0)
|
||||||
|
+ return;
|
||||||
|
+ glEnableVertexAttribArray(0);
|
||||||
|
+ glEnableVertexAttribArray(s_starLightAttrib);
|
||||||
|
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex), &s_starVerts[0].x);
|
||||||
|
+ glVertexAttribPointer(s_starLightAttrib, 1, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex),
|
||||||
|
+ &s_starVerts[0].light);
|
||||||
|
+ glDrawArrays(GL_POINTS, 0, s_starVertCount);
|
||||||
|
+ glDisableVertexAttribArray(0);
|
||||||
|
+ glDisableVertexAttribArray(s_starLightAttrib);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
// Shared by opengl_swap_and_restore()'s UI-overlay blit and
|
||||||
|
// android_composite_software_frame() below - draws an SDL_Surface as a
|
||||||
|
// fullscreen textured quad via gl4es. SDL's own renderer can't be used
|
||||||
|
@@ -537,42 +676,15 @@
|
||||||
|
|
||||||
|
set_blend_mode(blend);
|
||||||
|
|
||||||
|
- // Real per-vertex arrays, not glBegin/glVertexAttrib/glVertex3f
|
||||||
|
- // immediate mode: glVertexAttrib*() is GLES2's *constant*-attribute
|
||||||
|
- // API - without an enabled vertex array it sets one value for the
|
||||||
|
- // whole draw call, not a fresh value per vertex. gl4es's immediate-mode
|
||||||
|
- // emulation only reproduces desktop GL's per-vertex-capture behavior
|
||||||
|
- // for attribute 0 (position, driven directly by glVertex3f), not for
|
||||||
|
- // custom attributes like this shader's texcoords/light.
|
||||||
|
- //
|
||||||
|
// V is flipped (1 at the bottom, 0 at the top): GL texture v=0
|
||||||
|
// addresses the first row of data passed to glTexImage2D, but uiRgba
|
||||||
|
// (an SDL surface) stores its rows top-down.
|
||||||
|
- struct QuadVertex {
|
||||||
|
- float x, y, z;
|
||||||
|
- float u, v;
|
||||||
|
- float light;
|
||||||
|
- };
|
||||||
|
- static const QuadVertex quadVerts[4] = {
|
||||||
|
- {1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f},
|
||||||
|
- {1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f},
|
||||||
|
- {-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f},
|
||||||
|
- {-1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f},
|
||||||
|
- };
|
||||||
|
- glEnableVertexAttribArray(0);
|
||||||
|
- glEnableVertexAttribArray(tcAttrib);
|
||||||
|
- glEnableVertexAttribArray(lightAttrib);
|
||||||
|
- glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(QuadVertex), &quadVerts[0].x);
|
||||||
|
- glVertexAttribPointer(tcAttrib, 2, GL_FLOAT, GL_FALSE, sizeof(QuadVertex), &quadVerts[0].u);
|
||||||
|
- glVertexAttribPointer(lightAttrib, 1, GL_FLOAT, GL_FALSE, sizeof(QuadVertex),
|
||||||
|
- &quadVerts[0].light);
|
||||||
|
- glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
- // The engine's other rendering uses glBegin/glVertexAttrib/glVertex3f
|
||||||
|
- // immediate mode for attribute 0, which depends on it not having an
|
||||||
|
- // enabled array bound.
|
||||||
|
- glDisableVertexAttribArray(0);
|
||||||
|
- glDisableVertexAttribArray(tcAttrib);
|
||||||
|
- glDisableVertexAttribArray(lightAttrib);
|
||||||
|
+ android_imm_begin(tcAttrib, lightAttrib);
|
||||||
|
+ android_imm_vertex(1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
+ android_imm_vertex(1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
+ android_imm_vertex(-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
+ android_imm_vertex(-1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
+ android_imm_end();
|
||||||
|
// glFinish(), not glFlush(): xr_swapchain_release() (called by our
|
||||||
|
// caller right after this returns) hands the image to the OpenXR
|
||||||
|
// runtime for cross-process consumption, which requires the GPU write
|
||||||
|
@@ -642,6 +754,16 @@
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // Real per-vertex arrays, not glBegin/glVertexAttrib/glVertex3f
|
||||||
|
+ // immediate mode - see android_imm_begin()'s comment above for why.
|
||||||
|
+ android_imm_begin(tcAttrib, lightAttrib);
|
||||||
|
+ android_imm_vertex(1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
+ android_imm_vertex(1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
+ android_imm_vertex(-1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
+ android_imm_vertex(-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
+ android_imm_end();
|
||||||
|
+#else
|
||||||
|
glBegin(GL_TRIANGLE_STRIP);
|
||||||
|
glVertexAttrib2f(tcAttrib, 1.0f, 0.0f);
|
||||||
|
glVertexAttrib1f(lightAttrib, 1.0f);
|
||||||
|
@@ -656,6 +778,7 @@
|
||||||
|
glVertexAttrib1f(lightAttrib, 1.0f);
|
||||||
|
glVertex3f(-1.0f, 1.0f, 0.0f);
|
||||||
|
glEnd();
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
// Finish drawing the 3d view
|
||||||
|
glFlush();
|
||||||
|
@@ -709,7 +832,10 @@
|
||||||
|
render_width = width;
|
||||||
|
render_height = height;
|
||||||
|
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
int lw, lh;
|
||||||
|
SDL_RenderGetLogicalSize(renderer, &lw, &lh);
|
||||||
|
@@ -729,7 +855,10 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool opengl_cache_texture(CachedTexture toCache, grs_bitmap *bm) {
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
// GL_UNPACK_ROW_LENGTH must describe the buffer actually passed to
|
||||||
|
// glTexImage2D() below (toCache.converted->pixels, a separately
|
||||||
|
@@ -771,7 +900,10 @@
|
||||||
|
|
||||||
|
DEBUG("Clearing OpenGL texture cache.");
|
||||||
|
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
for (auto iter = texturesByBitsPtr.begin(); iter != texturesByBitsPtr.end();) {
|
||||||
|
// don't free locked surfaces
|
||||||
|
@@ -896,10 +1028,16 @@
|
||||||
|
light = 1.0f - (clut - grd_screen->ltab) / 4096.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ (void)tcAttrib; // captured by android_imm_begin() instead - see its comment above
|
||||||
|
+ android_imm_vertex(vertex.x / 65536.0f, vertex.y / 65536.0f, -vertex.z / 65536.0f,
|
||||||
|
+ vertex.uv.u / 256.0f, vertex.uv.v / 256.0f, light);
|
||||||
|
+#else
|
||||||
|
if (tcAttrib >= 0)
|
||||||
|
glVertexAttrib2f(tcAttrib, vertex.uv.u / 256.0, vertex.uv.v / 256.0);
|
||||||
|
glVertexAttrib1f(lightAttrib, light);
|
||||||
|
glVertex3f(vertex.x / 65536.0f, vertex.y / 65536.0f, -vertex.z / 65536.0f);
|
||||||
|
+#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int opengl_draw_tmap(int n, g3s_phandle *vp, grs_bitmap *bm) { return opengl_light_tmap(n, vp, bm); }
|
||||||
|
@@ -910,7 +1048,10 @@
|
||||||
|
return CLIP_ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
set_blend_mode(bm->flags & BMF_TRANS);
|
||||||
|
|
||||||
|
glUseProgram(textureShaderProgram.shaderProgram);
|
||||||
|
@@ -928,13 +1069,21 @@
|
||||||
|
glUniformMatrix4fv(textureShaderProgram.uniView, 1, false, ViewMatrix);
|
||||||
|
glUniformMatrix4fv(textureShaderProgram.uniProj, 1, false, ProjectionMatrix);
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ android_imm_begin(tcAttrib, lightAttrib);
|
||||||
|
+#else
|
||||||
|
glBegin(GL_TRIANGLE_STRIP);
|
||||||
|
+#endif
|
||||||
|
draw_vertex(*(vp[1]), tcAttrib, lightAttrib);
|
||||||
|
draw_vertex(*(vp[0]), tcAttrib, lightAttrib);
|
||||||
|
draw_vertex(*(vp[2]), tcAttrib, lightAttrib);
|
||||||
|
if (n > 3)
|
||||||
|
draw_vertex(*(vp[3]), tcAttrib, lightAttrib);
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ android_imm_end();
|
||||||
|
+#else
|
||||||
|
glEnd();
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
return CLIP_NONE;
|
||||||
|
}
|
||||||
|
@@ -949,7 +1098,10 @@
|
||||||
|
return CLIP_ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
set_blend_mode(bm->flags & BMF_TRANS);
|
||||||
|
|
||||||
|
glUseProgram(textureShaderProgram.shaderProgram);
|
||||||
|
@@ -976,6 +1128,14 @@
|
||||||
|
|
||||||
|
glUniform1i(textureShaderProgram.uniMutant, (bm->type == BMT_TLUC8) || (bm->flags == BMF_TLUC8));
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ android_imm_begin(tcAttrib, lightAttrib);
|
||||||
|
+ android_imm_vertex(convx(vpl[1]->x), convy(vpl[1]->y), 0.0f, 1.0f, 0.0f, light);
|
||||||
|
+ android_imm_vertex(convx(vpl[0]->x), convy(vpl[0]->y), 0.0f, 0.0f, 0.0f, light);
|
||||||
|
+ android_imm_vertex(convx(vpl[2]->x), convy(vpl[2]->y), 0.0f, 1.0f, 1.0f, light);
|
||||||
|
+ android_imm_vertex(convx(vpl[3]->x), convy(vpl[3]->y), 0.0f, 0.0f, 1.0f, light);
|
||||||
|
+ android_imm_end();
|
||||||
|
+#else
|
||||||
|
glBegin(GL_TRIANGLE_STRIP);
|
||||||
|
glVertexAttrib2f(tcAttrib, 1.0f, 0.0f);
|
||||||
|
glVertexAttrib1f(lightAttrib, light);
|
||||||
|
@@ -990,6 +1150,7 @@
|
||||||
|
glVertexAttrib1f(lightAttrib, light);
|
||||||
|
glVertex3f(convx(vpl[3]->x), convy(vpl[3]->y), 0.0f);
|
||||||
|
glEnd();
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
glUniform1i(textureShaderProgram.uniMutant, false);
|
||||||
|
|
||||||
|
@@ -1007,7 +1168,10 @@
|
||||||
|
return CLIP_ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
glUseProgram(colorShaderProgram.shaderProgram);
|
||||||
|
glUniformMatrix4fv(colorShaderProgram.uniView, 1, false, ViewMatrix);
|
||||||
|
@@ -1064,7 +1228,11 @@
|
||||||
|
GLint lightAttrib = colorShaderProgram.lightAttrib;
|
||||||
|
|
||||||
|
long a = 0, b = n_verts - 1;
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ android_imm_begin(-1, lightAttrib);
|
||||||
|
+#else
|
||||||
|
glBegin(GL_TRIANGLE_STRIP);
|
||||||
|
+#endif
|
||||||
|
while (true) {
|
||||||
|
draw_vertex(*(p[a]), -1, lightAttrib);
|
||||||
|
if (a++ == b)
|
||||||
|
@@ -1073,7 +1241,11 @@
|
||||||
|
if (b-- == a)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ android_imm_end();
|
||||||
|
+#else
|
||||||
|
glEnd();
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
return CLIP_NONE;
|
||||||
|
}
|
||||||
|
@@ -1085,7 +1257,10 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
void opengl_begin_stars() {
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
// Bind the star shader before setting the point size: on Android,
|
||||||
|
// questshock_glPointSize() (see the __ANDROID__ block above) needs
|
||||||
|
@@ -1108,11 +1283,19 @@
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ android_stars_begin(starShaderProgram.lightAttrib);
|
||||||
|
+#else
|
||||||
|
glBegin(GL_POINTS);
|
||||||
|
+#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void opengl_end_stars() {
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ android_stars_end();
|
||||||
|
+#else
|
||||||
|
glEnd();
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
// Turn off the stencil test and blending
|
||||||
|
opengl_set_stencil(0x00);
|
||||||
|
@@ -1120,9 +1303,11 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
int opengl_draw_star(fix star_x, fix star_y, int c, bool anti_alias) {
|
||||||
|
+#ifndef __ANDROID__
|
||||||
|
+ // See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
-
|
||||||
|
GLint lightAttrib = starShaderProgram.lightAttrib;
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
float x = fix_float(star_x);
|
||||||
|
float y = fix_float(star_y);
|
||||||
|
@@ -1142,8 +1327,12 @@
|
||||||
|
int color = (std_color_base + std_color_range - 1 - c);
|
||||||
|
color = (255 * color) / (std_color_range + 1);
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ android_stars_vertex(x, y, -0.25f, color / 255.0f);
|
||||||
|
+#else
|
||||||
|
glVertexAttrib1f(lightAttrib, color / 255.0f);
|
||||||
|
glVertex3f(x, y, -0.25f);
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
return CLIP_NONE;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
--- a/src/GameSrc/frsetup.c
|
||||||
|
+++ b/src/GameSrc/frsetup.c
|
||||||
|
@@ -635,7 +635,27 @@
|
||||||
|
uchar old_cam_type;
|
||||||
|
int detail;
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // should_opengl_swap() (unlike use_opengl(), see its own
|
||||||
|
+ // FR_PICKUPM_MASK|FR_HACKCAM_MASK exclusion just below and the
|
||||||
|
+ // matching one used for the STEREO_SUPPORT path in fr_send_view()
|
||||||
|
+ // below) doesn't know which _fr context is currently being prepared -
|
||||||
|
+ // it's a frame-level check, also used by Shock.c's SDLDraw() to pick
|
||||||
|
+ // the frame's overall compositing path. render_hack_cameras() (see
|
||||||
|
+ // render.c) calls fr_rend() - and so this function - a second time per
|
||||||
|
+ // real frame for each active hack-camera monitor, rendering into a
|
||||||
|
+ // private software (FR_DOUBLEB_MASK) canvas rather than the actual
|
||||||
|
+ // GL-backed screen. Without this exclusion, opengl_start_frame() runs
|
||||||
|
+ // again mid-frame for that render even though its own polygon draw
|
||||||
|
+ // calls correctly stay on the software path (use_opengl() already
|
||||||
|
+ // excludes them) - re-entering the OpenXR swapchain/framebuffer setup
|
||||||
|
+ // a second time before the first call's frame has ended. Not known to
|
||||||
|
+ // be exercised on desktop (no OpenXR swapchain there), so this fix is
|
||||||
|
+ // kept Android-only rather than changing should_opengl_swap() itself.
|
||||||
|
+ if (should_opengl_swap() && !(_fr_curflags & (FR_PICKUPM_MASK | FR_HACKCAM_MASK))) {
|
||||||
|
+#else
|
||||||
|
if(should_opengl_swap()) {
|
||||||
|
+#endif
|
||||||
|
opengl_start_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -823,7 +843,14 @@
|
||||||
|
|
||||||
|
g3_end_frame();
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // Mirrors the same fix in fr_start_view() above - see its comment.
|
||||||
|
+ // opengl_start_frame()/opengl_end_frame() must stay paired per real
|
||||||
|
+ // GL frame, so this exclusion has to match that one exactly.
|
||||||
|
+ if (should_opengl_swap() && !(_fr_curflags & (FR_PICKUPM_MASK | FR_HACKCAM_MASK))) {
|
||||||
|
+#else
|
||||||
|
if(should_opengl_swap()) {
|
||||||
|
+#endif
|
||||||
|
opengl_end_frame();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
--- a/src/MacSrc/OpenGL.cc
|
||||||
|
+++ b/src/MacSrc/OpenGL.cc
|
||||||
|
@@ -455,10 +455,30 @@
|
||||||
|
#ifndef __ANDROID__
|
||||||
|
// See opengl_resize()'s comment above.
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
-#endif
|
||||||
|
|
||||||
|
// Done rendering to the frame buffer, reset back to normal
|
||||||
|
BindFrameBuffer(nullptr);
|
||||||
|
+#else
|
||||||
|
+ // BindFrameBuffer(nullptr) (glBindFramebuffer/glBindRenderbuffer to id
|
||||||
|
+ // 0) means "the real window surface" - on desktop that's SDL's actual
|
||||||
|
+ // GL window, but this app's real presentation target is never that
|
||||||
|
+ // SDLSurface's own EGL surface at all - it's the OpenXR swapchain's
|
||||||
|
+ // per-image FBOs, set up separately in xr_swapchain.c. Whatever
|
||||||
|
+ // framebuffer/renderbuffer 0 actually resolves to for gl4es in this
|
||||||
|
+ // configuration, binding it here reliably left gl4es reporting a
|
||||||
|
+ // persistent GL_INVALID_OPERATION (1282) - confirmed by bisecting
|
||||||
|
+ // through this whole render path with per-call checkpoints, all clean
|
||||||
|
+ // right up until "after opengl_end_frame", the one immediately after
|
||||||
|
+ // this exact call.
|
||||||
|
+ //
|
||||||
|
+ // The bind is also functionally dead on Android regardless of that:
|
||||||
|
+ // by the time anything next draws, Shock.c's SDLDraw() has already
|
||||||
|
+ // run xr_frame_begin() -> xr_swapchain_acquire(), which rebinds to
|
||||||
|
+ // that frame's real swapchain FBO before opengl_swap_and_restore()
|
||||||
|
+ // (the only thing that draws before the next opengl_start_frame()'s
|
||||||
|
+ // own BindFrameBuffer(&backupBuffer)) runs - so skipping it changes
|
||||||
|
+ // nothing about what actually gets rendered where.
|
||||||
|
+#endif
|
||||||
|
palette_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
--- a/src/MacSrc/Shock.c
|
||||||
|
+++ b/src/MacSrc/Shock.c
|
||||||
|
@@ -31,6 +31,7 @@
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
+#include "qs_config.h"
|
||||||
|
#include "xr_session.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@@ -109,6 +110,15 @@
|
||||||
|
LoadHotkeyKeybinds();
|
||||||
|
LoadMoveKeybinds();
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // Persistent, human-editable JSON config (questshock.config.json) - see
|
||||||
|
+ // qs_config.h/CONFIG.md. After prefs.txt is loaded (above) so this file
|
||||||
|
+ // can override it, before init_all() below so there's no live-vs-
|
||||||
|
+ // restart distinction to worry about yet - every field just takes full
|
||||||
|
+ // effect as part of this same startup.
|
||||||
|
+ qs_config_load_startup();
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
// Process some startup arguments
|
||||||
|
|
||||||
|
bool show_splash = !CheckArgument("-nosplash");
|
||||||
|
--- a/src/Libraries/INPUT/Source/sdl_events.c
|
||||||
|
+++ b/src/Libraries/INPUT/Source/sdl_events.c
|
||||||
|
@@ -29,6 +29,10 @@
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <OpenGL.h>
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+#include "qs_config.h"
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
extern SDL_Window *window;
|
||||||
|
extern SDL_Renderer *renderer;
|
||||||
|
|
||||||
|
@@ -481,6 +485,14 @@
|
||||||
|
SDL_Event ev;
|
||||||
|
|
||||||
|
while (SDL_PollEvent(&ev)) {
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // Custom event types qs_config_request_save()/_load() enqueue from
|
||||||
|
+ // the "Save Configuration"/"Load Configuration" menu buttons (see
|
||||||
|
+ // qs_config.h) - handled here, on this thread, rather than directly
|
||||||
|
+ // from those JNI calls (which run on Android's UI thread).
|
||||||
|
+ if (qs_config_handle_event(&ev))
|
||||||
|
+ continue;
|
||||||
|
+#endif
|
||||||
|
switch (ev.type) {
|
||||||
|
case SDL_QUIT:
|
||||||
|
// a bit hacky at this place, but this would allow exiting the game via the window's [x] button
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
--- a/src/GameSrc/mainloop.c
|
||||||
|
+++ b/src/GameSrc/mainloop.c
|
||||||
|
@@ -57,6 +57,10 @@
|
||||||
|
#include "tools.h"
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+#include "qs_config.h"
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
// how is the game doing, anyway, set to true at end of time
|
||||||
|
uchar cit_success = FALSE;
|
||||||
|
|
||||||
|
@@ -103,6 +107,16 @@
|
||||||
|
_last_mode = *cmode;
|
||||||
|
(*exit_modes[_last_mode])();
|
||||||
|
*cmode = _new_mode;
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // Notifies qs_config.c of every loop-mode transition so it can pick
|
||||||
|
+ // between the "Initial Screen" and "Game Screen" virtual-screen
|
||||||
|
+ // presets - see qs_config.h/CONFIG.md. Called synchronously, directly,
|
||||||
|
+ // on this thread (mainloop()'s own) - unlike the UI-thread-triggered
|
||||||
|
+ // config actions elsewhere in qs_config.c, this doesn't need the
|
||||||
|
+ // cross-thread SDL-event dance since it already originates on the
|
||||||
|
+ // right thread.
|
||||||
|
+ qs_screen_profile_notify_loop_mode(*cmode);
|
||||||
|
+#endif
|
||||||
|
_static_change = 0;
|
||||||
|
if (*cmode >= 0)
|
||||||
|
(*enter_modes[*cmode])();
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
--- a/src/GameSrc/cutsloop.c
|
||||||
|
+++ b/src/GameSrc/cutsloop.c
|
||||||
|
@@ -292,11 +292,38 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+// The intro (cutscene 0) ships in one file per language - localized
|
||||||
|
+// voice-over - each holding the same resource id (cutscene_anims[0]), so
|
||||||
|
+// opening the current language's file instead is all it takes. The death
|
||||||
|
+// and ending cutscenes only have one version.
|
||||||
|
+static char *intro_files_by_language[3] =
|
||||||
|
+{
|
||||||
|
+ "res/data/svgaintr.res", // English
|
||||||
|
+ "res/data/svfrintr.res", // French
|
||||||
|
+ "res/data/svgeintr.res" // German
|
||||||
|
+};
|
||||||
|
+
|
||||||
|
+static int open_cutscene_file(int id)
|
||||||
|
+{
|
||||||
|
+ if (id == 0 && which_lang > 0 && which_lang < 3)
|
||||||
|
+ {
|
||||||
|
+ int handle = ResOpenFile(intro_files_by_language[(int)which_lang]);
|
||||||
|
+ if (handle > 0)
|
||||||
|
+ return handle;
|
||||||
|
+ // Not installed for this language - fall back to English below.
|
||||||
|
+ }
|
||||||
|
+ return ResOpenFile(cutscene_files[id]);
|
||||||
|
+}
|
||||||
|
+#else
|
||||||
|
+#define open_cutscene_file(id) ResOpenFile(cutscene_files[id])
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
short play_cutscene(int id, bool show_credits)
|
||||||
|
{
|
||||||
|
MacTuneKillCurrentTheme();
|
||||||
|
|
||||||
|
- cutscene_filehandle = ResOpenFile(cutscene_files[id]);
|
||||||
|
+ cutscene_filehandle = open_cutscene_file(id);
|
||||||
|
if (cutscene_filehandle <= 0) {
|
||||||
|
// If we failed to play the cutscene, go to setup / credits.
|
||||||
|
_new_mode = SETUP_LOOP;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
--- a/src/GameSrc/mouselook.c
|
||||||
|
+++ b/src/GameSrc/mouselook.c
|
||||||
|
@@ -121,3 +121,10 @@
|
||||||
|
get_mouselook_vel(&mvelx, &mvely);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+// Whether the player is currently in cyberspace; read by the Android
|
||||||
|
+// controller input code (android/app/src/main/cpp/xr_input.c) to switch
|
||||||
|
+// between the normal and cyberspace action sets.
|
||||||
|
+int qs_in_cyberspace(void) { return global_fullmap != NULL && global_fullmap->cyber; }
|
||||||
|
+#endif
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
--- a/src/GameSrc/movekeys.c
|
||||||
|
+++ b/src/GameSrc/movekeys.c
|
||||||
|
@@ -44,6 +44,12 @@
|
||||||
|
extern bool gKeypadOverride;
|
||||||
|
|
||||||
|
uchar parse_motion_key(ushort code, short *cnum, short *cval);
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+// Set every frame by android/app/src/main/cpp/xr_input.c.
|
||||||
|
+float qs_cyber_pitch_scale = 1.0f;
|
||||||
|
+float qs_cyber_bank_scale = 1.0f;
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
uchar parse_motion_key_cyber(ushort code, short *cnum, short *cval);
|
||||||
|
void init_motion_polling(void);
|
||||||
|
|
||||||
|
@@ -173,6 +179,20 @@
|
||||||
|
*cnum = -1;
|
||||||
|
*cval = 0;
|
||||||
|
|
||||||
|
+#ifdef __ANDROID__
|
||||||
|
+ // Pitch and bank strength, scaled by the controller thumbstick's
|
||||||
|
+ // deflection (see android/app/src/main/cpp/xr_input.c); 1.0 for a key.
|
||||||
|
+ short pitchMax = (short)(CONTROL_MAX_VAL * qs_cyber_pitch_scale + 0.5f);
|
||||||
|
+ short bankMax = (short)(CONTROL_MAX_VAL * qs_cyber_bank_scale + 0.5f);
|
||||||
|
+ if (pitchMax < 1)
|
||||||
|
+ pitchMax = 1;
|
||||||
|
+ if (bankMax < 1)
|
||||||
|
+ bankMax = 1;
|
||||||
|
+#else
|
||||||
|
+ short pitchMax = CONTROL_MAX_VAL;
|
||||||
|
+ short bankMax = CONTROL_MAX_VAL;
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
while (MoveCyberKeybinds[i].code != 255) {
|
||||||
|
if (code == MoveCyberKeybinds[i].code) {
|
||||||
|
move = MoveCyberKeybinds[i].move;
|
||||||
|
@@ -189,22 +209,22 @@
|
||||||
|
|
||||||
|
case M_CLIMB:
|
||||||
|
*cnum = CONTROL_YVEL;
|
||||||
|
- *cval = -CONTROL_MAX_VAL;
|
||||||
|
+ *cval = -pitchMax;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case M_BANKLEFT:
|
||||||
|
*cnum = CONTROL_XYROT;
|
||||||
|
- *cval = -CONTROL_MAX_VAL;
|
||||||
|
+ *cval = -bankMax;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case M_BANKRIGHT:
|
||||||
|
*cnum = CONTROL_XYROT;
|
||||||
|
- *cval = CONTROL_MAX_VAL;
|
||||||
|
+ *cval = bankMax;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case M_DIVE:
|
||||||
|
*cnum = CONTROL_YVEL;
|
||||||
|
- *cval = CONTROL_MAX_VAL;
|
||||||
|
+ *cval = pitchMax;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case M_ROLLRIGHT:
|
||||||
|
@@ -219,38 +239,38 @@
|
||||||
|
|
||||||
|
case M_CLIMBLEFT:
|
||||||
|
*cnum = CONTROL_YVEL;
|
||||||
|
- *cval = -CONTROL_MAX_VAL;
|
||||||
|
+ *cval = -pitchMax;
|
||||||
|
if (abs(poll_controls[*cnum]) < abs(*cval))
|
||||||
|
poll_controls[*cnum] = *cval;
|
||||||
|
*cnum = CONTROL_XYROT;
|
||||||
|
- *cval = -CONTROL_MAX_VAL;
|
||||||
|
+ *cval = -bankMax;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case M_CLIMBRIGHT:
|
||||||
|
*cnum = CONTROL_YVEL;
|
||||||
|
- *cval = -CONTROL_MAX_VAL;
|
||||||
|
+ *cval = -pitchMax;
|
||||||
|
if (abs(poll_controls[*cnum]) < abs(*cval))
|
||||||
|
poll_controls[*cnum] = *cval;
|
||||||
|
*cnum = CONTROL_XYROT;
|
||||||
|
- *cval = CONTROL_MAX_VAL;
|
||||||
|
+ *cval = bankMax;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case M_DIVERIGHT:
|
||||||
|
*cnum = CONTROL_YVEL;
|
||||||
|
- *cval = CONTROL_MAX_VAL;
|
||||||
|
+ *cval = pitchMax;
|
||||||
|
if (abs(poll_controls[*cnum]) < abs(*cval))
|
||||||
|
poll_controls[*cnum] = *cval;
|
||||||
|
*cnum = CONTROL_XYROT;
|
||||||
|
- *cval = CONTROL_MAX_VAL;
|
||||||
|
+ *cval = bankMax;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case M_DIVELEFT:
|
||||||
|
*cnum = CONTROL_YVEL;
|
||||||
|
- *cval = CONTROL_MAX_VAL;
|
||||||
|
+ *cval = pitchMax;
|
||||||
|
if (abs(poll_controls[*cnum]) < abs(*cval))
|
||||||
|
poll_controls[*cnum] = *cval;
|
||||||
|
*cnum = CONTROL_XYROT;
|
||||||
|
- *cval = -CONTROL_MAX_VAL;
|
||||||
|
+ *cval = -bankMax;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unhandled movement. What I gonna do?
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#Thu Nov 11 18:20:34 PST 2021
|
#Sat Sep 05 16:12:16 CEST 2026
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ fi
|
|||||||
echo "prebuiltDir=$PREBUILT_DIR_PROP"
|
echo "prebuiltDir=$PREBUILT_DIR_PROP"
|
||||||
} > "$ANDROID_DIR/engine.properties"
|
} > "$ANDROID_DIR/engine.properties"
|
||||||
|
|
||||||
echo "== Staging bundled assets (shaders, soundfont, get-assets text) =="
|
echo "== Staging bundled assets (shaders, soundfont, get-assets text, default config) =="
|
||||||
ASSETS_DIR="$ANDROID_DIR/app/src/main/assets"
|
ASSETS_DIR="$ANDROID_DIR/app/src/main/assets"
|
||||||
rm -rf "$ASSETS_DIR"
|
rm -rf "$ASSETS_DIR"
|
||||||
mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res"
|
mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res"
|
||||||
@@ -91,6 +91,8 @@ mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res"
|
|||||||
cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/"
|
cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/"
|
||||||
cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2"
|
cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2"
|
||||||
cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GET_ASSETS_QUEST.txt"
|
cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GET_ASSETS_QUEST.txt"
|
||||||
|
cp "$REPO_ROOT/res/questshock.config.default.json" "$ASSETS_DIR/questshock.config.default.json"
|
||||||
|
cp -a "$REPO_ROOT/res/controller_profiles" "$ASSETS_DIR/controller_profiles"
|
||||||
|
|
||||||
echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/openxr .so's into jniLibs =="
|
echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/openxr .so's into jniLibs =="
|
||||||
# gl4es is deliberately absent here - it's no longer prebuilt, it's compiled
|
# gl4es is deliberately absent here - it's no longer prebuilt, it's compiled
|
||||||
|
|||||||
@@ -13,10 +13,23 @@
|
|||||||
# inside it in sshock.kpf, which is just a zip file. Its res/pc/hd/ and
|
# inside it in sshock.kpf, which is just a zip file. Its res/pc/hd/ and
|
||||||
# res/pc/cdrom/ trees together hold every classic-game data/sound file
|
# res/pc/cdrom/ trees together hold every classic-game data/sound file
|
||||||
# Shockolate's res/ folder expects (res/pc/hd wins the couple of
|
# Shockolate's res/ folder expects (res/pc/hd wins the couple of
|
||||||
# filenames present in both: intro.res, objprop.dat). Needs innoextract
|
# filenames present in both: intro.res, objprop.dat), EXCEPT
|
||||||
# and unzip; if they're not installed locally, this script does the
|
# mfdfrn.res/mfdger.res (French/German MFD art - see CONFIG.md/README.md),
|
||||||
# extraction inside a throwaway Ubuntu container instead (needs docker in
|
# which that installer doesn't include at all - see "Optional: French/
|
||||||
# that case, nothing else).
|
# German MFD art" below for where those instead come from. Needs
|
||||||
|
# innoextract and unzip; if they're not installed locally, this script
|
||||||
|
# does the extraction inside a throwaway Ubuntu container instead (needs
|
||||||
|
# docker in that case, nothing else).
|
||||||
|
#
|
||||||
|
# Optional: French/German MFD art
|
||||||
|
# The Enhanced Edition installer's data set doesn't include
|
||||||
|
# mfdfrn.res/mfdger.res, so French/German are otherwise missing their
|
||||||
|
# MFD (Multi-Function Display) art and the game refuses to switch to
|
||||||
|
# them (see CONFIG.md). System Shock: Classic (also gog.com) does
|
||||||
|
# include both files. If you also own it, drop its offline installer
|
||||||
|
# (e.g. setup_system_shock_classic_2.0.0.3.exe) in this directory too -
|
||||||
|
# entirely optional, this script still runs fine without it - and this
|
||||||
|
# script pulls just those two files out of it into ss_ee/data/ as well.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
@@ -27,6 +40,7 @@ fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
|||||||
|
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
installers=(setup_system_shock_enhanced_edition*.exe)
|
installers=(setup_system_shock_enhanced_edition*.exe)
|
||||||
|
classic_installers=(setup_system_shock_classic*.exe)
|
||||||
shopt -u nullglob
|
shopt -u nullglob
|
||||||
case "${#installers[@]}" in
|
case "${#installers[@]}" in
|
||||||
0) fail "no setup_system_shock_enhanced_edition*.exe found in $ASSETS_DIR - buy System Shock: Enhanced Edition on gog.com, download the offline installer, and drop it here" ;;
|
0) fail "no setup_system_shock_enhanced_edition*.exe found in $ASSETS_DIR - buy System Shock: Enhanced Edition on gog.com, download the offline installer, and drop it here" ;;
|
||||||
@@ -35,6 +49,15 @@ case "${#installers[@]}" in
|
|||||||
esac
|
esac
|
||||||
INSTALLER="${installers[0]}"
|
INSTALLER="${installers[0]}"
|
||||||
|
|
||||||
|
# Optional - see "Optional: French/German MFD art" above. Empty (not
|
||||||
|
# missing/failed) when absent; only a genuine ambiguity (more than one
|
||||||
|
# candidate file) is a hard failure.
|
||||||
|
case "${#classic_installers[@]}" in
|
||||||
|
0) CLASSIC_INSTALLER="" ;;
|
||||||
|
1) CLASSIC_INSTALLER="${classic_installers[0]}" ;;
|
||||||
|
*) fail "multiple setup_system_shock_classic*.exe files found in $ASSETS_DIR - keep only one: ${classic_installers[*]}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Extracts sshock.kpf from $1 (an installer .exe) and assembles $2 (an
|
# Extracts sshock.kpf from $1 (an installer .exe) and assembles $2 (an
|
||||||
# ss_ee output directory) from its res/pc/{hd,cdrom} trees. Defined as a
|
# ss_ee output directory) from its res/pc/{hd,cdrom} trees. Defined as a
|
||||||
# function so the exact same logic can run either natively or, via
|
# function so the exact same logic can run either natively or, via
|
||||||
@@ -69,9 +92,38 @@ run_extraction() {
|
|||||||
cp -a "$work/kpf/res/pc/hd/sound/." "$out_dir/sound/"
|
cp -a "$work/kpf/res/pc/hd/sound/." "$out_dir/sound/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Extracts mfdfrn.res/mfdger.res from $1 (a System Shock: Classic installer
|
||||||
|
# .exe) into $2 (an existing ss_ee output directory's data/ subfolder) -
|
||||||
|
# see "Optional: French/German MFD art" above. Unlike the Enhanced Edition
|
||||||
|
# installer, Classic's data files sit directly in the Inno Setup package
|
||||||
|
# (no intermediate zip), so this is just a targeted innoextract --include,
|
||||||
|
# no unzip step needed. Same declare -f-into-docker structure as
|
||||||
|
# run_extraction() above.
|
||||||
|
run_classic_mfd_extraction() {
|
||||||
|
set -euo pipefail
|
||||||
|
local installer="$1" out_dir="$2"
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${work:-}"' EXIT
|
||||||
|
|
||||||
|
echo "== Extracting mfdfrn.res/mfdger.res from classic installer =="
|
||||||
|
innoextract --silent \
|
||||||
|
--include 'app/SSHOCK/DATA/MFDFRN.RES' --include 'app/SSHOCK/DATA/MFDGER.RES' \
|
||||||
|
-d "$work/classic" "$installer"
|
||||||
|
|
||||||
|
# Original DOS-era filenames are uppercase; Shockolate's resource
|
||||||
|
# loader expects the lowercase names already used for every other
|
||||||
|
# res/data/*.res file (case-sensitive on Linux, unlike the DOS/Windows
|
||||||
|
# filesystems these came from).
|
||||||
|
cp "$work/classic/app/SSHOCK/DATA/MFDFRN.RES" "$out_dir/data/mfdfrn.res"
|
||||||
|
cp "$work/classic/app/SSHOCK/DATA/MFDGER.RES" "$out_dir/data/mfdger.res"
|
||||||
|
}
|
||||||
|
|
||||||
if command -v innoextract >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1; then
|
if command -v innoextract >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1; then
|
||||||
echo "== Using local innoextract/unzip =="
|
echo "== Using local innoextract/unzip =="
|
||||||
run_extraction "$ASSETS_DIR/$INSTALLER" "$OUT_DIR"
|
run_extraction "$ASSETS_DIR/$INSTALLER" "$OUT_DIR"
|
||||||
|
if [ -n "$CLASSIC_INSTALLER" ]; then
|
||||||
|
run_classic_mfd_extraction "$ASSETS_DIR/$CLASSIC_INSTALLER" "$OUT_DIR"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "== innoextract/unzip not found locally - using a throwaway docker container =="
|
echo "== innoextract/unzip not found locally - using a throwaway docker container =="
|
||||||
command -v docker >/dev/null 2>&1 \
|
command -v docker >/dev/null 2>&1 \
|
||||||
@@ -86,12 +138,18 @@ else
|
|||||||
apt-get update -qq >/dev/null
|
apt-get update -qq >/dev/null
|
||||||
apt-get install -y -qq innoextract unzip >/dev/null
|
apt-get install -y -qq innoextract unzip >/dev/null
|
||||||
$(declare -f run_extraction)
|
$(declare -f run_extraction)
|
||||||
|
$(declare -f run_classic_mfd_extraction)
|
||||||
run_extraction '/assets/$INSTALLER' '/assets/ss_ee'
|
run_extraction '/assets/$INSTALLER' '/assets/ss_ee'
|
||||||
|
$([ -n "$CLASSIC_INSTALLER" ] && echo "run_classic_mfd_extraction '/assets/$CLASSIC_INSTALLER' '/assets/ss_ee'")
|
||||||
chown -R \"\$HOST_UID:\$HOST_GID\" /assets/ss_ee
|
chown -R \"\$HOST_UID:\$HOST_GID\" /assets/ss_ee
|
||||||
"
|
"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== Done =="
|
echo "== Done =="
|
||||||
echo "Assets extracted to $OUT_DIR"
|
echo "Assets extracted to $OUT_DIR"
|
||||||
echo "Note: mfdfrn.res/mfdger.res (French/German MFD art) are not present in this"
|
if [ -n "$CLASSIC_INSTALLER" ]; then
|
||||||
echo "installer and are only needed for those localizations - not for English play."
|
echo "Also extracted mfdfrn.res/mfdger.res (French/German MFD art) from $CLASSIC_INSTALLER."
|
||||||
|
else
|
||||||
|
echo "Note: mfdfrn.res/mfdger.res (French/German MFD art) are not present - only needed for"
|
||||||
|
echo "French/German (see CONFIG.md); drop a System Shock: Classic installer here too to get them."
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Generic OpenXR Controller",
|
||||||
|
"interaction_profile": "/interaction_profiles/khr/simple_controller",
|
||||||
|
"image": "generic.png",
|
||||||
|
"controls": [
|
||||||
|
{"id": "left_trigger", "label": "Select (Left)", "anchor_x": 0.30, "anchor_y": 0.60},
|
||||||
|
{"id": "right_trigger", "label": "Select (Right)", "anchor_x": 0.30, "anchor_y": 0.60}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "Meta Quest Touch",
|
||||||
|
"interaction_profile": "/interaction_profiles/oculus/touch_controller",
|
||||||
|
"image": "touch.png",
|
||||||
|
"controls": [
|
||||||
|
{"id": "left_trigger", "label": "Trigger", "anchor_x": 0.30, "anchor_y": 0.45},
|
||||||
|
{"id": "right_trigger", "label": "Trigger", "anchor_x": 0.72, "anchor_y": 0.45},
|
||||||
|
{"id": "left_squeeze", "label": "Grip", "anchor_x": 0.12, "anchor_y": 0.55},
|
||||||
|
{"id": "right_squeeze", "label": "Grip", "anchor_x": 0.58, "anchor_y": 0.55},
|
||||||
|
{"id": "left_x", "label": "X", "anchor_x": 0.14, "anchor_y": 0.33},
|
||||||
|
{"id": "left_y", "label": "Y", "anchor_x": 0.24, "anchor_y": 0.35},
|
||||||
|
{"id": "right_a", "label": "A", "anchor_x": 0.68, "anchor_y": 0.35},
|
||||||
|
{"id": "right_b", "label": "B", "anchor_x": 0.75, "anchor_y": 0.33},
|
||||||
|
{"id": "left_thumbstick", "label": "Thumbstick", "anchor_x": 0.20, "anchor_y": 0.28},
|
||||||
|
{"id": "right_thumbstick", "label": "Thumbstick", "anchor_x": 0.62, "anchor_y": 0.30},
|
||||||
|
{"id": "left_thumbstick_click", "label": "Thumbstick Click", "anchor_x": 0.20, "anchor_y": 0.28},
|
||||||
|
{"id": "right_thumbstick_click", "label": "Thumbstick Click", "anchor_x": 0.62, "anchor_y": 0.30}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 62 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "HTC Vive Wand",
|
||||||
|
"interaction_profile": "/interaction_profiles/htc/vive_controller",
|
||||||
|
"image": "vive.png",
|
||||||
|
"controls": [
|
||||||
|
{"id": "left_trigger", "label": "Trigger", "anchor_x": 0.13, "anchor_y": 0.62},
|
||||||
|
{"id": "right_trigger", "label": "Trigger", "anchor_x": 0.78, "anchor_y": 0.62},
|
||||||
|
{"id": "left_squeeze", "label": "Grip", "anchor_x": 0.08, "anchor_y": 0.68},
|
||||||
|
{"id": "right_squeeze", "label": "Grip", "anchor_x": 0.85, "anchor_y": 0.68},
|
||||||
|
{"id": "left_thumbstick", "label": "Trackpad", "anchor_x": 0.20, "anchor_y": 0.42},
|
||||||
|
{"id": "right_thumbstick", "label": "Trackpad", "anchor_x": 0.72, "anchor_y": 0.42},
|
||||||
|
{"id": "left_thumbstick_click", "label": "Trackpad Click", "anchor_x": 0.20, "anchor_y": 0.42},
|
||||||
|
{"id": "right_thumbstick_click", "label": "Trackpad Click", "anchor_x": 0.72, "anchor_y": 0.42}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"video_options": {
|
||||||
|
"renderer": "opengl",
|
||||||
|
"resolution": "1024x768",
|
||||||
|
"initial_screen_width_m": 1.600000,
|
||||||
|
"initial_screen_distance_m": 2.000000,
|
||||||
|
"game_screen_width_m": 3.400000,
|
||||||
|
"game_screen_distance_m": 2.000000,
|
||||||
|
"screen_width_offset_m": 1.000000,
|
||||||
|
"screen_width_step_m": 0.600000,
|
||||||
|
"screen_distance_offset_m": 1.000000,
|
||||||
|
"screen_distance_step_m": 0.500000
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"language": "english"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 435 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 283 KiB |