mirror of
https://github.com/Pixelplanet/Pixel-VR-Player.git
synced 2026-10-23 11:01:37 +02:00
760 lines
32 KiB
C++
760 lines
32 KiB
C++
#include "app/app.hpp"
|
|
|
|
#include "util/logging.hpp"
|
|
|
|
#include <cctype>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace pixelvr {
|
|
|
|
namespace {
|
|
|
|
std::string to_lower(std::string s) {
|
|
for (char& c : s) {
|
|
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
}
|
|
return s;
|
|
}
|
|
|
|
bool contains(const std::string& s, const char* token) {
|
|
return s.find(token) != std::string::npos;
|
|
}
|
|
|
|
// True if `needle` appears not immediately followed by a digit, so a marker like
|
|
// "360" matches but a resolution like "3600" (or "3600p") does not.
|
|
bool contains_num(const std::string& s, const char* needle) {
|
|
const std::string n = needle;
|
|
std::size_t pos = 0;
|
|
while ((pos = s.find(n, pos)) != std::string::npos) {
|
|
const std::size_t after = pos + n.size();
|
|
if (after >= s.size() || s[after] < '0' || s[after] > '9') return true;
|
|
pos = after;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Infers projection and stereo layout from common filename conventions, e.g.
|
|
// "clip_360_tb.mp4", "movie_180_sbs.mkv", "trailer_vr180.mp4". `stereoFromName`
|
|
// reports whether the stereo layout was stated explicitly (so the caller can
|
|
// refine it from the frame aspect ratio otherwise).
|
|
void detect_format(const std::string& path, VulkanRenderer::ProjectionMode& proj,
|
|
VulkanRenderer::StereoMode& stereo, bool& stereoFromName) {
|
|
const std::string p = to_lower(path);
|
|
|
|
// Check 180 before 360 and ignore digit runs so "vr180.3600p" is 180, not 360
|
|
// (the resolution "3600" must not match the "360" marker).
|
|
if (contains(p, "vr180") || contains_num(p, "180")) {
|
|
proj = VulkanRenderer::ProjectionMode::Equirect180;
|
|
} else if (contains_num(p, "360") || contains(p, "equirect")) {
|
|
proj = VulkanRenderer::ProjectionMode::Equirect360;
|
|
} else {
|
|
proj = VulkanRenderer::ProjectionMode::Flat;
|
|
}
|
|
|
|
stereoFromName = true;
|
|
if (contains(p, "_tb") || contains(p, "-tb") || contains(p, "_ou") ||
|
|
contains(p, "overunder") || contains(p, "over-under") ||
|
|
contains(p, "htab") || contains(p, "h-tab")) {
|
|
stereo = VulkanRenderer::StereoMode::TopBottom;
|
|
} else if (contains(p, "sbs") || contains(p, "_lr") ||
|
|
contains(p, "sidebyside") || contains(p, "side-by-side")) {
|
|
stereo = VulkanRenderer::StereoMode::SideBySide; // incl. HSBS / half-SBS
|
|
} else if (contains(p, "3d")) {
|
|
// Bare "3D" with no explicit layout: half-SBS dominates movie rips.
|
|
stereo = VulkanRenderer::StereoMode::SideBySide;
|
|
} else {
|
|
stereo = VulkanRenderer::StereoMode::Mono;
|
|
stereoFromName = false; // no explicit layout; refine from aspect ratio
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool App::init(const std::string& mediaPath) {
|
|
if (!xr_.createInstanceAndSystem("Pixel VR Player")) {
|
|
return false;
|
|
}
|
|
if (!renderer_.createDevice(xr_)) {
|
|
return false;
|
|
}
|
|
if (!xr_.createSession(renderer_.graphicsBinding())) {
|
|
return false;
|
|
}
|
|
if (!xr_.createReferenceSpace()) {
|
|
return false;
|
|
}
|
|
if (!xr_.createActions()) {
|
|
PIXELVR_LOG_WARN("Controller input unavailable; continuing without it");
|
|
}
|
|
renderModels_.init(xr_.instance(), xr_.session(), xr_.renderModelEnabled());
|
|
|
|
const std::vector<int64_t> formats = xr_.enumerateSwapchainFormats();
|
|
if (formats.empty()) {
|
|
PIXELVR_LOG_ERROR("Runtime reported no swapchain formats");
|
|
return false;
|
|
}
|
|
const int64_t colorFormat = renderer_.chooseColorFormat(formats);
|
|
if (!xr_.createSwapchains(colorFormat)) {
|
|
return false;
|
|
}
|
|
if (!renderer_.createRenderResources(xr_.swapchains())) {
|
|
return false;
|
|
}
|
|
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
if (!mediaPath.empty()) {
|
|
loadMedia(mediaPath);
|
|
}
|
|
#else
|
|
(void)mediaPath;
|
|
#endif
|
|
|
|
PIXELVR_LOG_INFO("Initialization complete");
|
|
return true;
|
|
}
|
|
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
void App::loadMedia(const std::string& path) {
|
|
const bool sameFile = (path == currentMediaPath_);
|
|
currentMediaPath_ = path;
|
|
VulkanRenderer::ProjectionMode proj = VulkanRenderer::ProjectionMode::Flat;
|
|
VulkanRenderer::StereoMode stereo = VulkanRenderer::StereoMode::Mono;
|
|
bool stereoFromName = false;
|
|
detect_format(path, proj, stereo, stereoFromName);
|
|
|
|
renderer_.releaseVideoFrame(); // free the previous clip's dma-buf before reopening
|
|
renderer_.setSphereZoom(1.0f); // reset the 360/180 viewpoint per clip
|
|
renderer_.setSpherePitch(0.0f);
|
|
media_.setPreferredAudioLanguage(settings_.audioLanguage());
|
|
if (!sameFile) { // a new file starts from automatic track selection
|
|
media_.setPreferredVideoStream(-1);
|
|
media_.setPreferredAudioStream(-1);
|
|
}
|
|
mediaOpen_ = media_.open(path);
|
|
if (!mediaOpen_) {
|
|
// Clear stale video from the previous clip so the renderer shows black
|
|
// instead of the last decoded frame of the old file.
|
|
renderer_.clearVideo();
|
|
PIXELVR_LOG_WARN("Failed to open %s", path.c_str());
|
|
return;
|
|
}
|
|
|
|
// Refine the stereo layout from the frame aspect ratio when the filename did
|
|
// not state it: once the projection is a VR type the aspect is unambiguous
|
|
// (360 mono 2:1, SBS 4:1, OU 1:1; 180 mono 1:1, SBS 2:1, OU 1:2).
|
|
const int w = media_.width();
|
|
const int h = media_.height();
|
|
if (!stereoFromName && h > 0) {
|
|
const float ar = static_cast<float>(w) / static_cast<float>(h);
|
|
if (proj == VulkanRenderer::ProjectionMode::Equirect360) {
|
|
stereo = ar >= 3.0f ? VulkanRenderer::StereoMode::SideBySide
|
|
: ar <= 1.3f ? VulkanRenderer::StereoMode::TopBottom
|
|
: VulkanRenderer::StereoMode::Mono;
|
|
} else if (proj == VulkanRenderer::ProjectionMode::Equirect180) {
|
|
stereo = ar >= 1.6f ? VulkanRenderer::StereoMode::SideBySide
|
|
: ar <= 0.7f ? VulkanRenderer::StereoMode::TopBottom
|
|
: VulkanRenderer::StereoMode::Mono;
|
|
} else { // Flat: full-frame SBS roughly doubles the width.
|
|
stereo = ar >= 2.9f ? VulkanRenderer::StereoMode::SideBySide
|
|
: VulkanRenderer::StereoMode::Mono;
|
|
}
|
|
}
|
|
|
|
renderer_.setProjectionMode(proj);
|
|
renderer_.setStereoMode(stereo);
|
|
settings_.setProjectionMode(static_cast<int>(proj));
|
|
settings_.setStereoMode(static_cast<int>(stereo));
|
|
// Publish selectable tracks for the settings Audio/Video section.
|
|
std::vector<std::pair<int, std::string>> vlist, alist;
|
|
for (const auto& s : media_.videoStreams()) vlist.emplace_back(s.index, s.label);
|
|
for (const auto& s : media_.audioStreams()) alist.emplace_back(s.index, s.label);
|
|
settings_.setStreams(vlist, alist, media_.currentVideoStream(),
|
|
media_.currentAudioStream());
|
|
static const char* const kProj[] = {"flat", "360", "180"};
|
|
static const char* const kStereo[] = {"mono", "SBS", "TB"};
|
|
PIXELVR_LOG_INFO("Loaded %s (%dx%d, %s/%s)", path.c_str(), w, h,
|
|
kProj[static_cast<int>(proj)],
|
|
kStereo[static_cast<int>(stereo)]);
|
|
}
|
|
|
|
void App::switchTracks(bool byLanguage) {
|
|
if (currentMediaPath_.empty() || !mediaOpen_) return;
|
|
const double pos = media_.position();
|
|
media_.setPreferredAudioLanguage(settings_.audioLanguage());
|
|
media_.setPreferredVideoStream(settings_.selectedVideoStream());
|
|
media_.setPreferredAudioStream(byLanguage ? -1
|
|
: settings_.selectedAudioStream());
|
|
loadMedia(currentMediaPath_); // same file: keeps the preferences set above
|
|
if (pos > 0.5 && media_.duration() > 0.0) media_.seek(pos);
|
|
}
|
|
|
|
void App::updateStats() {
|
|
using clock = std::chrono::steady_clock;
|
|
const auto now = clock::now();
|
|
++statsFrameAccum_;
|
|
if (lastStatsUpdate_.time_since_epoch().count() == 0) {
|
|
lastStatsUpdate_ = now;
|
|
statsLastDecoded_ = media_.decodedFrames();
|
|
return;
|
|
}
|
|
const double elapsed =
|
|
std::chrono::duration<double>(now - lastStatsUpdate_).count();
|
|
if (elapsed < 0.5) {
|
|
return; // refresh the readout ~twice a second
|
|
}
|
|
|
|
const double fps = statsFrameAccum_ / elapsed;
|
|
const uint64_t dec = media_.decodedFrames();
|
|
const double decFps = static_cast<double>(dec - statsLastDecoded_) / elapsed;
|
|
statsFrameAccum_ = 0;
|
|
statsLastDecoded_ = dec;
|
|
lastStatsUpdate_ = now;
|
|
|
|
// Aggregate CPU load from /proc/stat deltas.
|
|
float cpuPct = -1.0f;
|
|
{
|
|
std::ifstream f("/proc/stat");
|
|
std::string cpu;
|
|
unsigned long long u = 0, n = 0, s = 0, idle = 0, io = 0, irq = 0,
|
|
sirq = 0, steal = 0;
|
|
if (f >> cpu >> u >> n >> s >> idle >> io >> irq >> sirq >> steal) {
|
|
const unsigned long long idleAll = idle + io;
|
|
const unsigned long long total =
|
|
u + n + s + idle + io + irq + sirq + steal;
|
|
if (cpuPrevTotal_ != 0 && total > cpuPrevTotal_) {
|
|
const unsigned long long dtot = total - cpuPrevTotal_;
|
|
const unsigned long long didle = idleAll - cpuPrevIdle_;
|
|
cpuPct = 100.0f * static_cast<float>(dtot - didle) /
|
|
static_cast<float>(dtot);
|
|
}
|
|
cpuPrevTotal_ = total;
|
|
cpuPrevIdle_ = idleAll;
|
|
}
|
|
}
|
|
|
|
// RAM usage from /proc/meminfo.
|
|
long memTotalKb = 0, memAvailKb = 0;
|
|
{
|
|
std::ifstream f("/proc/meminfo");
|
|
std::string key, unit;
|
|
long val = 0;
|
|
while (f >> key >> val >> unit) {
|
|
if (key == "MemTotal:") memTotalKb = val;
|
|
else if (key == "MemAvailable:") { memAvailKb = val; break; }
|
|
}
|
|
}
|
|
|
|
// Adreno GPU: the Frame runs the Turnip/msm driver (no kgsl sysfs), so report
|
|
// the GPU clock from devfreq (the governor scales it with load; pegged at max
|
|
// means GPU-bound).
|
|
long gpuCurMhz = 0, gpuMaxMhz = 0;
|
|
{
|
|
std::ifstream cf("/sys/class/devfreq/3d00000.gpu/cur_freq");
|
|
std::ifstream mf("/sys/class/devfreq/3d00000.gpu/max_freq");
|
|
long cur = 0, mx = 0;
|
|
if (cf >> cur) gpuCurMhz = cur / 1000000;
|
|
if (mf >> mx) gpuMaxMhz = mx / 1000000;
|
|
}
|
|
|
|
char buf[160];
|
|
statsLines_.clear();
|
|
std::snprintf(buf, sizeof(buf), "FPS %.1f (%.1f ms)", fps,
|
|
fps > 0.0 ? 1000.0 / fps : 0.0);
|
|
statsLines_.emplace_back(buf);
|
|
if (mediaOpen_) {
|
|
std::snprintf(buf, sizeof(buf), "Video %dx%d %s", media_.width(),
|
|
media_.height(), media_.codecName().c_str());
|
|
statsLines_.emplace_back(buf);
|
|
std::snprintf(buf, sizeof(buf), "Decode %.1f / %.2f fps", decFps,
|
|
media_.frameRate());
|
|
statsLines_.emplace_back(buf);
|
|
}
|
|
if (cpuPct >= 0.0f) {
|
|
std::snprintf(buf, sizeof(buf), "CPU %.0f%%", cpuPct);
|
|
statsLines_.emplace_back(buf);
|
|
}
|
|
if (gpuMaxMhz > 0) {
|
|
std::snprintf(buf, sizeof(buf), "GPU %ld/%ld MHz", gpuCurMhz, gpuMaxMhz);
|
|
} else {
|
|
std::snprintf(buf, sizeof(buf), "GPU n/a");
|
|
}
|
|
statsLines_.emplace_back(buf);
|
|
if (memTotalKb > 0) {
|
|
std::snprintf(buf, sizeof(buf), "RAM %ld / %ld MB",
|
|
(memTotalKb - memAvailKb) / 1024, memTotalKb / 1024);
|
|
statsLines_.emplace_back(buf);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
int App::navStep(float axisY, std::chrono::steady_clock::time_point now) {
|
|
using namespace std::chrono_literals;
|
|
if (axisY > 0.6f || axisY < -0.6f) {
|
|
const int dir = (axisY > 0.0f) ? -1 : 1; // push up = toward the top row
|
|
if (navArmed_) { // first press: step now, then pause before repeating
|
|
navArmed_ = false;
|
|
navNextRepeat_ = now + 400ms;
|
|
return dir;
|
|
}
|
|
if (now >= navNextRepeat_) { // held down: auto-repeat
|
|
navNextRepeat_ = now + 110ms;
|
|
return dir;
|
|
}
|
|
return 0;
|
|
}
|
|
if (axisY > -0.3f && axisY < 0.3f) navArmed_ = true; // released: re-arm
|
|
return 0;
|
|
}
|
|
|
|
int App::run() {
|
|
using namespace std::chrono_literals;
|
|
|
|
while (!exitRequested_) {
|
|
if (!xr_.pollEvents(exitRequested_) || exitRequested_) {
|
|
break;
|
|
}
|
|
|
|
if (xr_.sessionRunning()) {
|
|
// Compute frame delta-time for continuous seek (F4).
|
|
const auto now = std::chrono::steady_clock::now();
|
|
float dt = 0.0f;
|
|
if (lastFrameTime_.time_since_epoch().count() > 0) {
|
|
dt = std::chrono::duration<float>(now - lastFrameTime_).count();
|
|
dt = std::clamp(dt, 0.0f, 0.1f); // cap to avoid huge jumps
|
|
}
|
|
lastFrameTime_ = now;
|
|
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
// Release held dma-buf frames when the decoder is about to seek so it
|
|
// can reclaim its V4L2 capture buffers (otherwise it wedges).
|
|
if (mediaOpen_ && media_.needsFrameRelease()) {
|
|
renderer_.releaseVideoFrame();
|
|
media_.confirmFrameRelease();
|
|
}
|
|
if (mediaOpen_ && media_.drmActive()) {
|
|
// Zero-copy dma-buf path: import the decoder's frame directly.
|
|
if (media_.latestDrmFrame(drmFrame_)) {
|
|
renderer_.updateVideoDmabuf(drmFrame_);
|
|
}
|
|
} else if (mediaOpen_ && media_.latestFrame(frame_)) {
|
|
renderer_.updateVideoTexture(frame_.width, frame_.height,
|
|
frame_.y.data(), frame_.uv.data(),
|
|
frame_.colorspace, frame_.fullRange);
|
|
}
|
|
#endif
|
|
xr_.renderFrame([this](uint32_t viewIndex, uint32_t imageIndex,
|
|
const XrView& view, const ViewSwapchain& swapchain) {
|
|
renderer_.renderView(viewIndex, imageIndex, view, swapchain);
|
|
});
|
|
|
|
// Apply controller input that renderFrame synced this frame.
|
|
const XrInputState& in = xr_.input();
|
|
bool interacted = false;
|
|
bool browserOpen = false;
|
|
bool settingsOpen = false;
|
|
|
|
// Fetch the runtime-provided controller model once a controller is
|
|
// tracked (one-shot; subsequent calls return immediately).
|
|
if (in.controllers[0].active || in.controllers[1].active) {
|
|
renderModels_.poll();
|
|
}
|
|
|
|
// Live settings + place the screen in front on first head tracking.
|
|
renderer_.setSwapEyes(settings_.swapEyes());
|
|
renderer_.setDrawModels(settings_.drawModels());
|
|
if (!didInitialRecenter_ && in.headValid) {
|
|
renderer_.recenterScreen(in.headPos, in.headForward);
|
|
didInitialRecenter_ = true;
|
|
}
|
|
|
|
// Controller activity: moving a controller past a small threshold (or
|
|
// any button/stick input) keeps the laser, models and controls shown;
|
|
// after a few seconds of stillness they fade away.
|
|
const auto nowT = std::chrono::steady_clock::now();
|
|
bool activity = false;
|
|
for (int h = 0; h < 2; ++h) {
|
|
const auto& c = in.controllers[h];
|
|
if (!c.active) {
|
|
activityAnchorValid_[h] = false;
|
|
continue;
|
|
}
|
|
if (!activityAnchorValid_[h]) {
|
|
activityAnchorValid_[h] = true;
|
|
for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k];
|
|
for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k];
|
|
continue;
|
|
}
|
|
float dp = 0.0f;
|
|
for (int k = 0; k < 3; ++k) {
|
|
const float d = c.position[k] - activityAnchorPos_[h][k];
|
|
dp += d * d;
|
|
}
|
|
float od = 0.0f;
|
|
for (int k = 0; k < 4; ++k) od += c.orientation[k] * activityAnchorOrient_[h][k];
|
|
if (std::sqrt(dp) > 0.02f || std::fabs(od) < 0.995f) {
|
|
activity = true;
|
|
for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k];
|
|
for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k];
|
|
}
|
|
}
|
|
if (in.togglePlay || in.browserToggle || in.select || in.selectHeld ||
|
|
in.recenter || std::fabs(in.thumbstickX) > 0.15f ||
|
|
std::fabs(in.thumbstickY) > 0.15f ||
|
|
std::fabs(in.thumbstickRX) > 0.15f) {
|
|
activity = true;
|
|
}
|
|
if (activity) lastControllerActivity_ = nowT;
|
|
const bool controllersEngaged =
|
|
(nowT - lastControllerActivity_) < std::chrono::milliseconds(3000);
|
|
|
|
#ifdef PIXELVR_HAVE_FREETYPE
|
|
if (in.browserToggle) {
|
|
if (settings_.isOpen()) {
|
|
settings_.close();
|
|
} else if (browser_.isOpen()) {
|
|
browser_.setOpen(false);
|
|
} else {
|
|
browser_.open();
|
|
}
|
|
}
|
|
if (browser_.isOpen()) {
|
|
// A finished SMB download auto-plays and closes the browser.
|
|
std::string donePath;
|
|
if (browser_.poll(donePath) == Browser::Result::Play &&
|
|
!donePath.empty()) {
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
loadMedia(donePath);
|
|
#endif
|
|
browser_.setOpen(false);
|
|
}
|
|
}
|
|
// Publish the browser panel first so the pointer can raycast it.
|
|
if (browser_.isOpen()) {
|
|
std::vector<std::string> rows;
|
|
rows.reserve(browser_.entries().size());
|
|
for (const auto& e : browser_.entries()) {
|
|
rows.push_back(e.label);
|
|
}
|
|
std::string status = browser_.status();
|
|
if (browser_.onNetwork() && !browser_.isBusy()) {
|
|
status = "Point + Trigger: Stream A: Download";
|
|
}
|
|
renderer_.setBrowser(true, browser_.title(), rows,
|
|
browser_.selected(), status);
|
|
browserOpen = true;
|
|
} else {
|
|
renderer_.setBrowser(false, "", {}, 0, "");
|
|
}
|
|
#endif
|
|
|
|
// Settings panel overlay
|
|
if (settings_.isOpen()) {
|
|
std::vector<std::string> sRows;
|
|
std::vector<uint8_t> sEnabled;
|
|
sRows.reserve(settings_.rows().size());
|
|
sEnabled.reserve(settings_.rows().size());
|
|
for (const auto& r : settings_.rows()) {
|
|
sRows.push_back(r.value.empty() ? r.label
|
|
: (r.label + " : " + r.value));
|
|
sEnabled.push_back(r.enabled ? 1 : 0);
|
|
}
|
|
renderer_.setSettings(true, settings_.title(), sRows,
|
|
settings_.selected(), settings_.status(),
|
|
sEnabled);
|
|
settingsOpen = true;
|
|
} else {
|
|
renderer_.setSettings(false, "", {}, 0, "");
|
|
}
|
|
|
|
// Feed controller poses to the renderer (models, pointer ray, cursor);
|
|
// it raycasts the pointer against UI panels and reports hit rows/buttons.
|
|
{
|
|
auto vis = [](const XrInputState::ControllerPose& c) {
|
|
VulkanRenderer::ControllerVis v;
|
|
v.active = c.active;
|
|
for (int k = 0; k < 3; ++k) v.position[k] = c.position[k];
|
|
for (int k = 0; k < 4; ++k) v.orientation[k] = c.orientation[k];
|
|
return v;
|
|
};
|
|
const bool uiPanelActive = browserOpen || settingsOpen;
|
|
// Controls (buttons + timeline) show only while a controller is
|
|
// actively moving; they fade after a few seconds of stillness.
|
|
renderer_.setControlBar(!uiPanelActive && controllersEngaged &&
|
|
(in.controllers[0].active ||
|
|
in.controllers[1].active));
|
|
// Laser + models stay up while a panel is open (to point at it),
|
|
// otherwise they follow the same activity timeout.
|
|
const bool showLaser = uiPanelActive || controllersEngaged;
|
|
auto gated = [&](XrInputState::ControllerPose c) {
|
|
if (!showLaser) c.active = false;
|
|
return vis(c);
|
|
};
|
|
renderer_.setControllers(gated(in.controllers[0]),
|
|
gated(in.controllers[1]));
|
|
}
|
|
|
|
#ifdef PIXELVR_HAVE_FREETYPE
|
|
if (browser_.isOpen() && !browser_.isBusy()) {
|
|
if (const int d = navStep(in.thumbstickY, nowT)) {
|
|
browser_.move(d);
|
|
renderer_.scrollBrowser(d);
|
|
}
|
|
|
|
const int hovered = renderer_.browserHovered();
|
|
if (hovered >= 0) {
|
|
renderer_.setBrowserSelected(hovered);
|
|
if (in.select) {
|
|
browser_.setSelected(hovered);
|
|
std::string playPath;
|
|
if (browser_.activate(playPath) == Browser::Result::Play &&
|
|
!playPath.empty()) {
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
loadMedia(playPath);
|
|
#endif
|
|
browser_.setOpen(false);
|
|
}
|
|
}
|
|
}
|
|
if (in.togglePlay) {
|
|
browser_.downloadFocused();
|
|
}
|
|
}
|
|
if (browser_.isOpen() && in.recenter && in.headValid) {
|
|
renderer_.recenterScreen(in.headPos, in.headForward);
|
|
}
|
|
#endif
|
|
|
|
if (settings_.isOpen()) {
|
|
if (const int d = navStep(in.thumbstickY, nowT)) {
|
|
settings_.move(d);
|
|
}
|
|
|
|
const int hovered = renderer_.settingsHovered();
|
|
if (hovered >= 0) {
|
|
renderer_.setSettingsSelected(hovered);
|
|
if (in.select) {
|
|
settings_.setSelected(hovered);
|
|
auto act = settings_.activate();
|
|
if (act == SettingsMenu::Action::RecenterScreen) {
|
|
if (in.headValid) {
|
|
renderer_.recenterScreen(in.headPos, in.headForward);
|
|
}
|
|
} else if (act == SettingsMenu::Action::CycleProjection) {
|
|
int pm = settings_.projectionMode();
|
|
renderer_.setProjectionMode(
|
|
pm == 0 ? VulkanRenderer::ProjectionMode::Flat :
|
|
pm == 1 ? VulkanRenderer::ProjectionMode::Equirect360 :
|
|
VulkanRenderer::ProjectionMode::Equirect180);
|
|
} else if (act == SettingsMenu::Action::CycleStereo) {
|
|
int sm = settings_.stereoMode();
|
|
renderer_.setStereoMode(
|
|
sm == 0 ? VulkanRenderer::StereoMode::Mono :
|
|
sm == 1 ? VulkanRenderer::StereoMode::SideBySide :
|
|
VulkanRenderer::StereoMode::TopBottom);
|
|
} else if (act == SettingsMenu::Action::CycleDistance) {
|
|
renderer_.adjustScreenDistance(settings_.screenDistance() - 3.0f);
|
|
}
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
else if (act == SettingsMenu::Action::PickVideoStream ||
|
|
act == SettingsMenu::Action::PickAudioStream) {
|
|
switchTracks(false);
|
|
} else if (act == SettingsMenu::Action::PickLanguage) {
|
|
switchTracks(true);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
if (in.recenter && in.headValid) {
|
|
renderer_.recenterScreen(in.headPos, in.headForward);
|
|
}
|
|
}
|
|
|
|
if (!browserOpen && !settingsOpen) {
|
|
// Point-and-click the media control bar with the trigger.
|
|
const int btn = renderer_.pointerButton();
|
|
if (in.select && btn >= 0) {
|
|
const auto cb = static_cast<VulkanRenderer::CtrlButton>(btn);
|
|
if (cb == VulkanRenderer::CtrlButton::Stop) {
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
media_.close();
|
|
mediaOpen_ = false;
|
|
renderer_.clearVideo();
|
|
#endif
|
|
#ifdef PIXELVR_HAVE_FREETYPE
|
|
browser_.open(); // large file selection
|
|
#endif
|
|
} else if (cb == VulkanRenderer::CtrlButton::Settings) {
|
|
settings_.open();
|
|
} else if (cb == VulkanRenderer::CtrlButton::Recenter) {
|
|
if (in.headValid) {
|
|
renderer_.recenterScreen(in.headPos, in.headForward);
|
|
}
|
|
}
|
|
#ifdef PIXELVR_HAVE_FREETYPE
|
|
else if (cb == VulkanRenderer::CtrlButton::Files) {
|
|
browser_.open();
|
|
}
|
|
#endif
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
else if (mediaOpen_ &&
|
|
cb == VulkanRenderer::CtrlButton::PlayPause) {
|
|
media_.togglePause();
|
|
} else if (mediaOpen_ &&
|
|
cb == VulkanRenderer::CtrlButton::SeekBack) {
|
|
media_.seekRelative(-10.0);
|
|
} else if (mediaOpen_ &&
|
|
cb == VulkanRenderer::CtrlButton::SeekFwd) {
|
|
media_.seekRelative(10.0);
|
|
}
|
|
#endif
|
|
interacted = true;
|
|
}
|
|
if (in.select && renderer_.pointerOnTimeToggle()) {
|
|
renderer_.toggleTimeDisplay(); // total <-> time remaining
|
|
interacted = true;
|
|
}
|
|
if (in.recenter && in.headValid) {
|
|
renderer_.recenterScreen(in.headPos, in.headForward);
|
|
interacted = true;
|
|
}
|
|
// Viewpoint: flat screens move nearer/farther (thumbstick Y);
|
|
// immersive 360/180 uses the RIGHT stick -- up/down tilts (angle),
|
|
// left/right pushes content nearer/farther (distance).
|
|
if (settings_.projectionMode() == 0) {
|
|
if (in.thumbstickY > 0.15f || in.thumbstickY < -0.15f) {
|
|
renderer_.adjustScreenDistance(in.thumbstickY * 0.04f);
|
|
interacted = true;
|
|
}
|
|
} else if (dt > 0.0f) {
|
|
if (std::fabs(in.thumbstickRY) > 0.15f) {
|
|
renderer_.adjustSpherePitch(in.thumbstickRY * dt * 0.6f);
|
|
interacted = true;
|
|
}
|
|
if (std::fabs(in.thumbstickRX) > 0.15f) {
|
|
renderer_.adjustSphereZoom(in.thumbstickRX * dt * 0.6f);
|
|
interacted = true;
|
|
}
|
|
}
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
if (in.togglePlay && mediaOpen_) {
|
|
media_.togglePause();
|
|
interacted = true;
|
|
}
|
|
if (mediaOpen_) {
|
|
// Right stick left/right steps -/+30s (flat only; in 360/180 the
|
|
// right stick drives the viewpoint instead).
|
|
constexpr double kStep = 30.0;
|
|
const bool sphereMode = settings_.projectionMode() != 0;
|
|
if (!sphereMode && in.thumbstickRX > 0.6f && seekStepArmed_) {
|
|
media_.seekRelative(kStep);
|
|
seekStepArmed_ = false;
|
|
interacted = true;
|
|
} else if (!sphereMode && in.thumbstickRX < -0.6f && seekStepArmed_) {
|
|
media_.seekRelative(-kStep);
|
|
seekStepArmed_ = false;
|
|
interacted = true;
|
|
} else if (std::fabs(in.thumbstickRX) < 0.3f) {
|
|
seekStepArmed_ = true;
|
|
}
|
|
|
|
// Scrub: accumulate a target while dragging and commit it with a
|
|
// single seek on release. Seeking continuously floods the
|
|
// hardware decoder (a reopen each) and stalls it; one seek per
|
|
// gesture stays responsive.
|
|
bool scrubInput = false;
|
|
if (renderer_.pointerOnTimeline() && in.selectHeld &&
|
|
media_.duration() > 0.0) {
|
|
scrubTarget_ = renderer_.pointerScrub() * media_.duration();
|
|
scrubInput = true;
|
|
} else if (std::fabs(in.thumbstickLX) > 0.15f && dt > 0.0f &&
|
|
media_.duration() > 0.0) {
|
|
if (!scrubbing_) scrubTarget_ = media_.position();
|
|
const float ax = in.thumbstickLX;
|
|
const float t = (std::fabs(ax) - 0.15f) / 0.85f;
|
|
const double rate =
|
|
std::copysign(static_cast<double>(t * t) * 60.0, ax);
|
|
scrubTarget_ = std::clamp(scrubTarget_ + rate * dt, 0.0,
|
|
media_.duration());
|
|
scrubInput = true;
|
|
}
|
|
if (scrubInput) {
|
|
scrubbing_ = true;
|
|
interacted = true;
|
|
} else if (scrubbing_) {
|
|
media_.seek(scrubTarget_); // commit the scrub with one seek
|
|
scrubbing_ = false;
|
|
}
|
|
}
|
|
if (interacted) {
|
|
lastControllerActivity_ = nowT;
|
|
}
|
|
const bool paused = mediaOpen_ && media_.paused();
|
|
const bool controlsUp =
|
|
mediaOpen_ &&
|
|
(paused || (controllersEngaged && (in.controllers[0].active ||
|
|
in.controllers[1].active)));
|
|
float progress = 0.0f;
|
|
double elapsed = 0.0, total = 0.0;
|
|
if (mediaOpen_ && media_.duration() > 0.0) {
|
|
total = media_.duration();
|
|
elapsed = scrubbing_ ? scrubTarget_ : media_.position();
|
|
progress = static_cast<float>(elapsed / total);
|
|
}
|
|
renderer_.setTransport(controlsUp, progress, paused, elapsed, total);
|
|
#else
|
|
(void)interacted;
|
|
#endif
|
|
}
|
|
|
|
#ifdef PIXELVR_HAVE_MEDIA
|
|
if (settings_.showStats()) {
|
|
updateStats();
|
|
}
|
|
// Always-on periodic perf log so decode-bound vs render-bound stalls
|
|
// show up in the run log even without the on-screen overlay.
|
|
if (mediaOpen_) {
|
|
++perfFrames_;
|
|
if (perfLastLog_.time_since_epoch().count() == 0) {
|
|
perfLastLog_ = now;
|
|
perfLastDecoded_ = media_.decodedFrames();
|
|
} else {
|
|
const double since =
|
|
std::chrono::duration<double>(now - perfLastLog_).count();
|
|
if (since >= 2.0) {
|
|
const uint64_t dec = media_.decodedFrames();
|
|
PIXELVR_LOG_INFO(
|
|
"PERF: render %.1f fps, decode %.1f/%.2f fps",
|
|
perfFrames_ / since,
|
|
static_cast<double>(dec - perfLastDecoded_) / since,
|
|
media_.frameRate());
|
|
perfFrames_ = 0;
|
|
perfLastDecoded_ = dec;
|
|
perfLastLog_ = now;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
renderer_.setStats(settings_.showStats(), statsLines_);
|
|
} else {
|
|
// Idle until the runtime tells us to start rendering.
|
|
std::this_thread::sleep_for(50ms);
|
|
}
|
|
}
|
|
|
|
if (renderer_.device() != VK_NULL_HANDLE) {
|
|
vkDeviceWaitIdle(renderer_.device());
|
|
}
|
|
renderModels_.shutdown(); // destroy XR model handles before the session
|
|
PIXELVR_LOG_INFO("Shutting down");
|
|
return 0;
|
|
}
|
|
|
|
} // namespace pixelvr
|