ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PPSSPP Emulator API:通过 sceIoDevctl 的 “emulator:“ 伪设备为 Homebrew 打通模拟器专属能力

PPSSPP Emulator API:通过 sceIoDevctl 的 “emulator:“ 伪设备为 Homebrew 打通模拟器专属能力 PPSSPP Emulator API通过 sceIoDevctl 的 emulator: 伪设备为 Homebrew 打通模拟器专属能力【免费下载链接】ppssppA PSP emulator for Android, Windows, Mac, Linux and iOS, written in C. Want to contribute? Join us on Discord at https://discord.gg/5NJB6dD or just send pull requests / issues.项目地址: https://gitcode.com/GitHub_Trending/pp/ppssppppsspp_emu_api.h是 PPSSPP 官方提供的一个 header-only C 封装它让 PSP homebrew 程序能够通过一个真实存在的 PSP 系统调用sceIoDevctl探测自己正运行在 PPSSPP 上并进一步调用日志输出、截图、快进、显示参数查询、模拟轴/虚拟按键读取等模拟器专属功能。阅读本文后你将掌握如何零链接成本地把该头文件接入 PSPSDK 项目、EMULATOR_DEVCTL__*全部 10 条命令的编号与参数布局、GET_AXIS/GET_VKEY这类指针当索引的传参怪癖以及从源码级确认这些命令在 Core/HLE/sceIo.cpp 中的真实处理路径。一、API 定位一个系统调用承载全部模拟器私有协议整个 API 的设计核心只有一句话所有能力都复用标准的 PSP 系统调用sceIoDevctl只是设备名和 cmd 编号是 PPSSPP 私有的。sceIoDevctl本身是 PSP 上合法的系统调用标准签名int sceIoDevctl(const char *devicename, unsigned int cmd, void *indata, int inlen, void *outdata, int outlen);PPSSPP 在其中识别两个并不对应任何真实硬件的假设备名emulator:kemulator:从源码结构看这两个名字在入口处被完全等价处理并不存在用户态/内核态的区分因此用户态 homebrew 用哪个都能工作——Core/HLE/sceIo.cpp 中的判断是if (!strcmp(name, kemulator:) || !strcmp(name, emulator:))。该封装的关键特性Header-only零构建成本ppsspp_emu_api.h全部是static inline函数直接覆盖在sceIoDevctl之上没有需要编译或链接的库探测是安全的如果cmd不匹配任何已知命令sceIoDevctl返回错误UNKNOWN PARAMETERS而不是崩溃所以在真机或旧版本模拟器上探测支持性不会造成异常真机不可用是硬约束这一切在真实 PSP 硬件上都不存在必须在依赖任何功能之前先调用ppsspp_is_emulator()原始协议中对应IS_EMULATOR命令并且保留一条可运行的真机回退路径。二、快速上手把头文件拷进 PSPSDK 项目官方给出的接入方式非常简单把 ppsspp_emu_api.h 复制进基于 PSPSDK 的 homebrew 项目然后#include即可。官方 README 中的最小示例#include ppsspp_emu_api.h if (ppsspp_is_emulator()) { ppsspp_send_output_str(Hello from homebrew, running under PPSSPP!\n); }头文件内部的命令枚举与封装函数一一对应见 docs/emu-api/ppsspp_emu_api.henum PPSSPPEmulatorDevctlCmd { PPSSPP_DEVCTL__GET_HAS_DISPLAY 1, PPSSPP_DEVCTL__SEND_OUTPUT 2, PPSSPP_DEVCTL__IS_EMULATOR 3, PPSSPP_DEVCTL__VERIFY_STATE 4, PPSSPP_DEVCTL__EMIT_SCREENSHOT 0x20, PPSSPP_DEVCTL__TOGGLE_FASTFORWARD 0x30, PPSSPP_DEVCTL__GET_ASPECT_RATIO 0x31, PPSSPP_DEVCTL__GET_SCALE 0x32, PPSSPP_DEVCTL__GET_AXIS 0x33, PPSSPP_DEVCTL__GET_VKEY 0x34, }; // Either name works - PPSSPP currently treats them identically. #define PPSSPP_EMULATOR_DEVICE emulator:三、C 封装函数全解以下是ppsspp_emu_api.h中全部 10 个static inline函数的行为说明每个函数都只是一次sceIoDevctl调用的薄封装函数对应命令cmd 值方向 / 布局行为说明ppsspp_is_emulator()IS_EMULATOR3out:u32写入 1。必须第一个调用如果这次sceIoDevctl调用本身失败说明不在 PPSSPP或不实现该 API 的构建上运行其余函数全部无效。ppsspp_has_display()GET_HAS_DISPLAY1out:u32有真实显示输出普通 PPSSPP写 1headless 模式PPSSPPHeadless写 0。适合在无显示时跳过呈现/依赖 vblank 的工作。ppsspp_send_output(data, len)SEND_OUTPUT2in: bytes把一段原始文本直接送入 PPSSPP 的调试/日志输出headless 下还会进入其收集缓冲区无需通过sceIoWrite写真实文件。ppsspp_send_output_str(str)SEND_OUTPUT2in: bytes上面函数的 NUL 结尾字符串便捷版内部按strlen计算长度。ppsspp_verify_state()VERIFY_STATE4无让 PPSSPP 做一次内部存档往返存到内存再回读校验作为一致性检查。异步pass/fail 结果不会返回给你只在 PPSSPP 一侧打日志。主要价值是自动化测试模拟器本身。ppsspp_emit_screenshot()EMIT_SCREENSHOT0x20无抓取当前 framebuffer 并交给 PPSSPP 内部调试截图钩子pspautotests/frametest 基础设施用它收集结果图。注意它不写文件不是通用的截图存到记忆棒功能。ppsspp_toggle_fastforward(enable)TOGGLE_FASTFORWARD0x30in: boolNULL/非 NULL非 NULL 的indata开启快进NULL 关闭。ppsspp_get_aspect_ratio()GET_ASPECT_RATIO0x31out:float返回当前显示宽高比。目前仅在横屏方向下结果正确。ppsspp_get_scale()GET_SCALE0x32out:float返回当前显示缩放因子。目前仅在横屏方向下结果正确。ppsspp_get_axis(axisIndex)GET_AXIS0x33in: 轴索引作为indata指针值out:float读取 PPSSPP 侧输入插件注入的模拟轴值按JOYSTICK_AXIS_*索引。无插件激活时返回 0。ppsspp_get_vkey(keyCode)GET_VKEY0x34in: 键码作为indata指针值out:u8读取 PPSSPP 侧输入插件注入的虚拟键是否按下按 PPSSPP 内部NKCODE_*键码。无插件激活时返回 0。四、原始协议细节与常见陷阱如果你要扩展封装、为其他语言写绑定或者想彻底理解这套机制protocol.md 描述了原始sceIoDevctl协议以下几个易踩的坑值得逐条掌握4.1 设备名与调用约定设备名只能是emulator:或kemulator:两者当前行为一致参数布局随cmd变化多数命令在outdata写单个u32或floatSEND_OUTPUT把indata当作inlen长度的文本块TOGGLE_FASTFORWARD则只看indata是否为 NULL 作为布尔标志。4.2 指针当索引传参GET_AXIS / GET_VKEY 的核心怪癖这是最容易出错的一点GET_AXIS和GET_VKEY的输入不是indata缓冲区的内容而是indata指针的值本身被直接当作整数索引使用。这与当前 PPSSPP 实现的读法一致——实现里直接拿argAddr与轴/键范围比较后作为下标。调用方式示例sceIoDevctl(emulator:, EMULATOR_DEVCTL__GET_AXIS, (void *)JOYSTICK_AXIS_X, 0, value, sizeof(value));4.3 这不读普通手柄输入GET_AXIS/GET_VKEY读的是 PPSSPP HLE 插件系统的状态即由 PPSSPP 侧原生插件 PRX 显式设置给你的 homebrew 去取的值而不是sceCtrl*那套普通控制器输入普通输入请用sceCtrl*。没有插件激活时这些值就是 0/未按下。4.4 键码与轴索引来自 PPSSPP 内部枚举轴索引对应 PPSSPP 内部JOYSTICK_AXIS_*枚举、键码对应NKCODE_*枚举两者都定义在 Common/Input/KeyCodes.h与 PSP SDK 的任何枚举都不一致大体镜像 Android 的键/轴编码。常用值举例NKCODE_DPAD_UP / DPAD_DOWN / DPAD_LEFT / DPAD_RIGHT19/20/21…NKCODE_BUTTON_CROSS23、NKCODE_BUTTON_SQUARE99、NKCODE_BUTTON_TRIANGLE100、NKCODE_BUTTON_CIRCLE1004PPSSPP 自定义键码JOYSTICK_AXIS_X0、JOYSTICK_AXIS_Y1源码中还保留了一条历史包袱提醒偶尔能在旧测试代码里看到EMULATOR_DEVCTL__SEND_CTRLDATA0x10常量它当前并未被 PPSSPP 实现调用只会得到通用的unknown parameters错误不要依赖它。4.5 不使用封装头时的裸调用示例protocol.md 给出的 raw 调用示例等价于封装头的ppsspp_is_emulator()ppsspp_send_output_str()#include pspiofilemgr.h #define EMULATOR_DEVCTL__IS_EMULATOR 3 #define EMULATOR_DEVCTL__SEND_OUTPUT 2 int runningOnPPSSPP sceIoDevctl(emulator:, EMULATOR_DEVCTL__IS_EMULATOR, NULL, 0, NULL, 0) 0; if (runningOnPPSSPP) { const char *msg Hello from homebrew, running under PPSSPP!\n; sceIoDevctl(emulator:, EMULATOR_DEVCTL__SEND_OUTPUT, (void *)msg, strlen(msg), NULL, 0); }注意判定方式裸协议下是不是 PPSSPP靠的是调用本身返回 0而不是看输出缓冲区协议原文中IS_EMULATOR会向 out 写 1但示例选择了零输出参数 检查返回码的更简单形式。五、源码级实现每条命令在 PPSSPP 里发生了什么封装头的注释声明 Core/HLE/sceIo.cpp 是这套协议的 source of truth搜索emulator:即可定位。设备分支从 sceIoDevctl 的 L1992 开始命令 switch 位于 L2010-L2086。逐条对照GET_HAS_DISPLAYL2011-L2014写出的值由PSP_CoreParameter().headLess决定——headless 运行写 0普通运行写 1。这与headless/目录下的无头构建PPSSPPHeadless对应homebrew 可据此跳过无意义的呈现工作。SEND_OUTPUTL2015-L2022校验输入范围后把文本块交给Core_SendDebugOutput(LogLevel::LINFO, ...)若配置了collectDebugOutput回调数据还会追加进该缓冲区——这正是 headless 自动化测试收集输出的通道。VERIFY_STATEL2027-L2031直接调用SaveState::Verify()做存档往返一致性检查源码注释明确说明是异步的结果只出现在 PPSSPP 一侧日志里。EMIT_SCREENSHOTL2033-L2044通过__DisplayGetFramebuf取当前 frame buffer构造DebugScreenshotDesc高度固定 272即 PSP 竖屏分辨率交给Core_SendDebugScreenshot。这解释了为什么它服务于 frametest/pspautotests 图像收集而不是写文件的普通截图。TOGGLE_FASTFORWARDL2045-L2050argAddr非零置PSP_CoreParameter().fastForward true否则置 false——与协议文档NULL/非 NULL 即布尔的描述逐字对应。GET_ASPECT_RATIOL2051-L2064若启用了displayLayoutLandscape.bDisplayStretch直接取g_display的分辨率比否则取配置里的fDisplayAspectRatio乘以 PSP 基准比例 480/272。源码注释同样强调目前仅横屏正确。GET_SCALEL2065-L2073g_display.dp_xres * fDisplayScale / 480即以 PSP 480 宽为基准的整数倍率换算。GET_AXISL2074-L2078先校验argAddr落在[0, JOYSTICK_AXIS_MAX)内再从HLEPlugins::PluginDataAxis[argAddr]读出 float——这就是指针当索引的实锤。GET_VKEYL2079-L2083同理校验argAddr NKCODE_MAX后从HLEPlugins::GetKey(argAddr)读u8。兜底L2086任何未识别的 cmd 返回带UNKNOWN PARAMETERS文本的错误因此探测未知命令是安全的。5.1 插件输入数据的来源GET_AXIS/GET_VKEY背后的数据面定义在 Core/HLE/Plugins.h 与 Core/HLE/Plugins.cppfloat PluginDataAxis[JOYSTICK_AXIS_MAX]; // 轴数据直接按 JOYSTICK_AXIS_* 下标存放 std::mapint, uint8_t PluginDataKeys; // 键数据GetKey() 带互斥锁读取从源码结构看这些数据的写入方是 PPSSPP 宿主侧的输入处理代码而非游戏内插件主动设置UI/NativeApp.cpp 在处理轴输入时把设备上报的轴值axisId JOYSTICK_AXIS_MAX时写入HLEPlugins::PluginDataAxis[axis.axisId]UI/NativeApp.cpp 把重力传感器数据写入JOYSTICK_AXIS_ACCELEROMETER_X/Y/Z鼠标相对位移则写入JOYSTICK_AXIS_MOUSE_REL_X/Y另见 Windows/WindowsHost.cpp 与 Windows/main.cpp 中的等价写入键状态方面UI/NativeApp.cpp 在按键事件上调用HLEPlugins::SetKey(key.keyCode, down ? 1 : 0)更新PluginDataKeys。这意味着 homebrew 通过ppsspp_get_axis/ppsspp_get_vkey实际拿到的是宿主输入设备摇杆轴、倾斜、鼠标相对位移、键位经 PPSSPP 内部映射后的状态且读取路径带锁、按值返回对 homebrew 侧是只读快照。六、工程实践建议永远先探测再使用任何功能调用前调用ppsspp_is_emulator()裸协议下检查IS_EMULATOR调用是否返回 0并保留一条不依赖本 API 的真机回退路径——这是两份文档反复强调的第一原则。把 API 当可选增强日志输出SEND_OUTPUT、快进切换TOGGLE_FASTFORWARD、显示参数GET_ASPECT_RATIO/GET_SCALE都可以设计成有则用、无则退避免在真机或其他模拟器上功能断裂。区分普通输入与插件通道常规手柄/键盘输入继续走sceCtrl*只有需要 PPSSPP 宿主侧注入状态如测试自动化读取鼠标相对位移、倾斜轴时才用GET_AXIS/GET_VKEY并注意其索引语义是 PPSSPP 内部枚举而非 PSP SDK 枚举。不要依赖未实现命令SEND_CTRLDATA0x10在当前实现中不存在调用只会得到UNKNOWN PARAMETERS。横屏前提宽高比与缩放查询目前只在横屏方向下正确竖屏下拿到的值不应作为布局依据。七、小结PPSSPP 的 Emulator API 用一个标准系统调用 私有设备名与命令号的极简设计为 homebrew 打开了一条与模拟器宿主对话的通道能力面虽小探测、日志、存档自检、调试截图、快进、显示参数、插件输入但每一条都能在 Core/HLE/sceIo.cpp 中找到逐行可查的实现输入数据面则可追溯到 Core/HLE/Plugins.cpp 与 UI/NativeApp.cpp 的宿主输入管线。对 homebrew 开发者接入成本是复制一个头文件对想深入或扩展该协议的人docs/emu-api/protocol.md 加上sceIo.cpp中搜索emulator:就是全部所需的事实来源。【免费下载链接】ppssppA PSP emulator for Android, Windows, Mac, Linux and iOS, written in C. Want to contribute? Join us on Discord at https://discord.gg/5NJB6dD or just send pull requests / issues.项目地址: https://gitcode.com/GitHub_Trending/pp/ppsspp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表