Files
2026-10-14 04:20:24 +02:00

236 lines
8.5 KiB
C++

// Copyright (C) 2026 half144 and PS5 React contributors
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution term: see LICENSE-ATTRIBUTION.
#include "host_api.hpp"
#include "audio_sink.hpp"
#include "host_platform.hpp"
#include "storage_stats.hpp"
#include "audio/mixer.hpp"
#include <dirent.h>
#include <mach/mach.h>
#include <spawn.h>
#include <sys/mount.h>
#include <sys/statvfs.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
extern char** environ;
namespace {
SDL_GameController* controller = nullptr;
void sysctl_string(const char* name, char* out, std::size_t size) {
if (sysctlbyname(name, out, &size, nullptr, 0) != 0) *out = '\0';
}
// "pt_BR.UTF-8" → "pt-BR"; the C and POSIX locales name no language.
void environment_language(char* out, std::size_t size) {
const char* value = nullptr;
for (const char* name : {"PS5_REACT_LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"})
if ((value = std::getenv(name)) && *value) break;
if (!value || !*value || !std::strcmp(value, "C") || !std::strncmp(value, "C.", 2) || !std::strcmp(value, "POSIX")) return;
std::size_t length = std::strcspn(value, ".@");
if (length >= size) length = size - 1;
for (std::size_t i = 0; i < length; ++i) out[i] = value[i] == '_' ? '-' : value[i];
out[length] = '\0';
}
std::int64_t free_memory() {
vm_statistics64_data_t stats;
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
vm_size_t page = 0;
if (host_page_size(mach_host_self(), &page) != KERN_SUCCESS ||
host_statistics64(mach_host_self(), HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&stats), &count) != KERN_SUCCESS)
return -1;
return static_cast<std::int64_t>(stats.free_count + stats.inactive_count) * static_cast<std::int64_t>(page);
}
} // namespace
void desktop_set_controller(SDL_GameController* value) {
controller = value;
}
namespace host {
const char* platform_name() {
return "desktop";
}
bool relaunch = false;
bool arrange_relaunch(char*, std::size_t) {
relaunch = true;
return true;
}
int list_mounts(MountEntry* mounts, int max) {
struct statfs* found = nullptr;
const int count = getmntinfo(&found, MNT_NOWAIT);
if (count <= 0) return -1;
const int used = count < max ? count : max;
for (int i = 0; i < used; ++i) {
std::snprintf(mounts[i].device, sizeof mounts[i].device, "%s", found[i].f_mntfromname);
std::snprintf(mounts[i].path, sizeof mounts[i].path, "%s", found[i].f_mntonname);
std::snprintf(mounts[i].type, sizeof mounts[i].type, "%s", found[i].f_fstypename);
}
return used;
}
bool disk_usage(const char* real_path, double& total, double& free) {
struct statvfs fs;
if (statvfs(real_path, &fs) != 0) return false;
if (!storage::disk_bytes(fs.f_frsize, fs.f_blocks, static_cast<std::int64_t>(fs.f_bavail), total, free)) {
errno = EIO;
return false;
}
return true;
}
void trace(const char*) {}
bool read_dir(const char* real_path, void (*visit)(const char* name, void* user), void* user) {
DIR* dir = opendir(real_path);
if (!dir) return false;
while (const dirent* item = readdir(dir))
if (std::strcmp(item->d_name, ".") && std::strcmp(item->d_name, "..")) visit(item->d_name, user);
closedir(dir);
return true;
}
bool resolve_path(const char* path, char* out, std::size_t size) {
if (path[0] != '/' || std::strstr(path, "/../")) return false;
const std::size_t length = std::strlen(path);
if (length >= 3 && !std::strcmp(path + length - 3, "/..")) return false;
const char* root = std::getenv("PS5_REACT_SANDBOX");
if (!root || !*root) root = "./sandbox";
const int written = std::snprintf(out, size, "%s%s", root, length == 1 ? "" : path);
return written >= 0 && static_cast<std::size_t>(written) < size;
}
void device_info(DeviceInfo& info) {
char model[48];
sysctl_string("hw.model", model, sizeof model);
std::snprintf(info.model, sizeof info.model, "Desktop preview%s%s%s", *model ? " (" : "", model, *model ? ")" : "");
char version[24];
sysctl_string("kern.osproductversion", version, sizeof version);
if (*version) std::snprintf(info.firmware, sizeof info.firmware, "macOS %s", version);
std::uint64_t frequency = 0;
std::size_t size = sizeof frequency;
// Apple silicon does not publish a CPU frequency; it stays unknown there.
if (sysctlbyname("hw.cpufrequency", &frequency, &size, nullptr, 0) == 0)
info.cpu_frequency = static_cast<std::int64_t>(frequency);
info.free_memory = free_memory();
rusage usage;
if (getrusage(RUSAGE_SELF, &usage) == 0)
info.process_time = (static_cast<std::int64_t>(usage.ru_utime.tv_sec) + usage.ru_stime.tv_sec) * 1000000 +
usage.ru_utime.tv_usec + usage.ru_stime.tv_usec;
environment_language(info.language, sizeof info.language);
}
bool foreground_user(User& user) {
const char* name = std::getenv("USER");
user.id = 1;
std::snprintf(user.name, sizeof user.name, "%s", name && *name ? name : "player");
return true;
}
int logged_in_users(User* users, int max) {
return max > 0 && foreground_user(users[0]) ? 1 : 0;
}
bool notify(const char* message, const char* sub_message) {
std::printf("[notify] %s%s%s\n", message, *sub_message ? ": " : "", sub_message);
std::fflush(stdout);
return true;
}
bool open_url(const char* url) {
if (std::strncmp(url, "http://", 7) && std::strncmp(url, "https://", 8)) return false;
char open[] = "open";
char* argv[] = {open, const_cast<char*>(url), nullptr};
pid_t pid = 0;
if (posix_spawnp(&pid, "open", nullptr, nullptr, argv, environ) != 0) return false;
int status = 0;
// `open` hands the URL to Launch Services and exits at once.
return waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
void set_light_bar(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
if (!controller || SDL_GameControllerSetLED(controller, r, g, b) != 0)
std::printf("[pad] light bar %u,%u,%u (no controller LED)\n", r, g, b);
}
void reset_light_bar() {
// SDL has no reset; restore the DualSense player-one blue.
if (!controller || SDL_GameControllerSetLED(controller, 0, 0, 64) != 0)
std::puts("[pad] light bar reset (no controller LED)");
}
void vibrate(float strength, float seconds) {
const auto level = static_cast<Uint16>(strength * 0xffff);
if (!controller || SDL_GameControllerRumble(controller, level, level, static_cast<Uint32>(seconds * 1000)) != 0)
std::printf("[pad] vibrate %.2f for %.2fs (no controller rumble)\n", strength, seconds);
}
// SDL2 can send DualSense trigger effects only as raw report bytes; the preview logs the request instead.
void set_trigger_effect(unsigned mask, int mode, const std::uint8_t* params, std::size_t count) {
static constexpr const char* kModes[] = {"off", "feedback", "weapon", "vibration"};
std::printf("[pad] trigger effect mask=%u %s", mask, kModes[mode]);
for (std::size_t i = 0; i < count; ++i) std::printf(" %d", params[i]);
std::puts(" (not sent on desktop)");
}
// SDL keeps the display awake while a window is open; keepAwake(false) lets it sleep.
void keep_awake(bool enabled) {
if (enabled) SDL_DisableScreenSaver();
else SDL_EnableScreenSaver();
std::printf("[power] keep awake %s\n", enabled ? "on" : "off");
}
} // namespace host
namespace {
SDL_AudioDeviceID audio_device = 0;
void render_audio(void* mixer, Uint8* stream, int bytes) {
static_cast<hui::audio::Mixer*>(mixer)->render(reinterpret_cast<std::int16_t*>(stream), bytes / 4);
}
}
namespace host {
// SDL_AUDIODRIVER=dummy keeps the preview silent (the self-test sets it).
bool start_audio(hui::audio::Mixer& mixer) {
SDL_AudioSpec want = {};
want.freq = hui::audio::kSampleRate;
want.format = AUDIO_S16SYS;
want.channels = 2;
want.samples = 256;
want.callback = render_audio;
want.userdata = &mixer;
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0 ||
!(audio_device = SDL_OpenAudioDevice(nullptr, 0, &want, nullptr, 0))) {
std::printf("[sound] no audio output: %s\n", SDL_GetError());
return false;
}
SDL_PauseAudioDevice(audio_device, 0);
return true;
}
void stop_audio() {
SDL_CloseAudioDevice(audio_device);
audio_device = 0;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
// The preview has no HDMI sink to read or to send a bitstream to: video audio is always decoded.
bool has_bitstream() { return false; }
bool open_bitstream(iec61937::Codec, BitstreamFill) { return false; }
void close_bitstream() {}
bool sink_edid(std::vector<std::uint8_t>&) { return false; }
} // namespace host
bool desktop_relaunch_requested() { return host::relaunch; }