mirror of
https://github.com/mitch030504/AstroQuest-Frame.git
synced 2026-10-25 11:01:22 +01:00
The app was made for a DualSense and took everything else for a lesser one of those. Players without one could play on the PC, with the headset's controllers, and not on the headset. The headset's own controllers: the app reads them through the headset's runtime (OpenXR actions, the layout the PC host has: A cross, B square, X circle, Y triangle and both together to blow, grips L1/R1, triggers L2/R2, right stick the finger on the touchpad, left menu button OPTIONS, both sticks in to reset the view), the right one is the controller in the game with where it is and how it points, both shake with the game's rumble. The gamepad the system makes of them for other apps is no longer taken for a gamepad: it says neither where it is nor whether anybody holds it. Which of the two: a small router (quest-host/cpp/pad_router.h) between the activity, which hears of gamepads, and the session, which reads the controllers. The game is played with the one used last. From a gamepad only a button under a thumb or a stick takes over, never a trigger or a grip: while a gamepad is held it is hands the headset sees. What buttons mean about the view besides (the first press of X takes the seat, OPTIONS held resets the view) moved there from the activity, so that it is the same for both. The start-up menu takes either; what confirmed it is not pressed in the game. Gamepads that are not a PlayStation's: - The right stick as the touchpad's finger is Input::StickFinger now, as on the PC: the finger lifts where the stick was let go, which the catapult at the end of a level needs. A button that presses the touchpad touches it in its middle. - A gamepad without motion sensors followed the hands to where they were and kept pointing straight ahead. Now the hands turn and tilt it (quest-host/cpp/held_pad.h): the line between the palms is its sideways axis, the way the hands point is its tilt. The app says so with a new flag of the pad pose message (HeldInHands), and the emulator lets go of a controller that was a tracked device once it no longer is (it stayed where that device was last seen for as long as no motion sensor of its own spoke up). - Motion sensors are only taken from PlayStation controllers: which way is up for one is each maker's own affair. - A right stick on RX/RY, triggers that are only buttons, a Back button the system takes for its own "back". For trying a build on a headset whose owner plays with the installed one: quest-host/build.sh --test-package builds the app under another name, which installs next to it; the test tools take ASTRO_PACKAGE. The self-test can report a controller the way a session does (HOST_PAD) and keep pictures of it (HOST_SHOTS). Tried on a Quest 3 without wearing it: the runtime takes the bindings, the app starts and plays with a made-up gamepad, and in the game's picture the controller is where and how the self-test says a tracked controller or hands around a gamepad are, and comes back when they are lost. tools/tests/pad_router_test.cpp and held_pad_test.cpp try the rest on the PC. Not tried: any of it worn, and no gamepad other than a DualSense. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
5.0 KiB
C++
145 lines
5.0 KiB
C++
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
#pragma once
|
|
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include <sys/types.h>
|
|
|
|
#include "pad_state.h"
|
|
#include "vr_protocol.h"
|
|
|
|
/// Whether the game may listen to the headset's microphone, and how much louder than it is
|
|
/// what it hears should be made.
|
|
struct MicrophoneSettings {
|
|
bool enabled{true};
|
|
float gain{1.0f};
|
|
/// For tests nobody is there to blow for: instead of the microphone, a second of noise
|
|
/// every five, as loud as the game takes blowing hard to be.
|
|
bool test_signal{};
|
|
/// Set while the player blows with buttons instead of with their breath: the game then
|
|
/// hears that same noise, whatever the microphone has to say (or is allowed to).
|
|
std::shared_ptr<const std::atomic<bool>> blowing;
|
|
};
|
|
|
|
/// Everything needed to start the emulator core.
|
|
struct CoreLaunch {
|
|
std::string loader; ///< glibc's dynamic loader, inside the app's native library folder
|
|
std::string runtime_root; ///< extracted runtime: host/ (libraries, core), drivers/
|
|
std::string storage_root; ///< writable folder the core keeps its data in
|
|
std::string game; ///< path of the game's eboot.bin
|
|
std::string log_file;
|
|
std::vector<std::string> extra_args;
|
|
std::vector<std::pair<std::string, std::string>> extra_env;
|
|
};
|
|
|
|
/// Runs the emulator core as a child process and serves the sockets it talks to: the control
|
|
/// line protocol (state out, controller in), audio, and the VR frame/pose channel.
|
|
class CoreProcess {
|
|
public:
|
|
enum class State { Idle, Starting, Running, Stopped, Failed };
|
|
|
|
CoreProcess() = default;
|
|
~CoreProcess();
|
|
|
|
bool Start(const CoreLaunch& launch);
|
|
void Stop();
|
|
|
|
State GetState() const {
|
|
return state.load();
|
|
}
|
|
/// Human-readable reason once the state is Stopped or Failed.
|
|
std::string GetMessage();
|
|
/// Frames the core reported as presented so far.
|
|
uint64_t GetPresentedFrames() const {
|
|
return presented_frames.load();
|
|
}
|
|
|
|
void SetPad(const PadState& pad);
|
|
|
|
/// Freezes the emulator while nobody is looking (headset taken off), or lets it go on.
|
|
void SetPaused(bool paused);
|
|
|
|
/// Offers the frame buffers to the core. `fds` are dma-bufs, they stay owned by the caller.
|
|
void SetBuffers(const std::vector<int>& fds, uint32_t width, uint32_t height, uint32_t stride);
|
|
void SendPose(const Core::Vr::Protocol::Pose& pose);
|
|
void SendPadPose(const Core::Vr::Protocol::PadPose& pose);
|
|
void SendOptics(float ipd);
|
|
/// The display refreshes: the emulated headset keeps step with it (`rate` in Hz).
|
|
void SendRefresh(float rate);
|
|
/// Returns the newest frame the core finished since the last call, if any.
|
|
std::optional<Core::Vr::Protocol::Frame> TakeFrame();
|
|
|
|
/// What the game last asked of the controller, packed: small motor, large motor, red,
|
|
/// green, blue, one byte each from the lowest up.
|
|
uint64_t GetPadFeedback() const {
|
|
return pad_feedback.load();
|
|
}
|
|
|
|
/// What the emulator process has used: processor time since it started, the processors it
|
|
/// may run on (as the system lists them, "0-5") and its memory.
|
|
struct Usage {
|
|
double cpu_seconds{};
|
|
std::string cpus;
|
|
int memory_mb{};
|
|
};
|
|
Usage GetUsage() const;
|
|
|
|
/// Applies to microphone ports the game opens from now on.
|
|
void SetMicrophone(bool enabled, float gain, bool test_signal = false);
|
|
/// The player blows with buttons, or has stopped: what the game hears follows at once.
|
|
void SetBlowing(bool blowing_) {
|
|
blowing->store(blowing_, std::memory_order_relaxed);
|
|
}
|
|
|
|
/// Thread id of the core's thread whose name starts with `prefix`, or 0.
|
|
pid_t FindThread(const std::string& prefix) const;
|
|
/// All threads of the emulator whose names begin with `prefix`.
|
|
std::vector<pid_t> FindThreads(const std::string& prefix) const;
|
|
|
|
private:
|
|
void ControlLoop();
|
|
void AudioLoop();
|
|
void VrLoop();
|
|
void WaitLoop();
|
|
void SendBuffersLocked();
|
|
void Fail(const std::string& message);
|
|
|
|
std::atomic<State> state{State::Idle};
|
|
std::atomic<bool> quit{};
|
|
std::atomic<bool> paused{};
|
|
std::atomic<uint64_t> presented_frames{};
|
|
std::atomic<uint64_t> pad_feedback{};
|
|
std::atomic<pid_t> pid{-1};
|
|
|
|
int control_listen{-1};
|
|
int audio_listen{-1};
|
|
int vr_listen{-1};
|
|
std::atomic<int> control_fd{-1};
|
|
std::atomic<int> vr_fd{-1};
|
|
|
|
std::thread control_thread;
|
|
std::thread audio_thread;
|
|
std::thread vr_thread;
|
|
std::thread wait_thread;
|
|
|
|
MicrophoneSettings microphone;
|
|
/// Shared with the threads that serve the game's microphone ports, which may outlast this.
|
|
std::shared_ptr<std::atomic<bool>> blowing{std::make_shared<std::atomic<bool>>(false)};
|
|
|
|
std::mutex mutex;
|
|
std::string message;
|
|
PadState pad;
|
|
std::optional<Core::Vr::Protocol::Frame> frame;
|
|
std::vector<int> buffer_fds;
|
|
Core::Vr::Protocol::Buffers buffers{};
|
|
bool buffers_sent{};
|
|
};
|