Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
- name: Test archive and overlay tools
run: |
python3 profiles/mhfu/tests/tool_security_tests.py
python3 profiles/mhfu/tests/android_packaging_tests.py
python3 -m unittest discover -s scripts/ci -p 'test_*.py'

- name: Run tests
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ test: build

python-test:
$(PYTHON) profiles/mhfu/tests/tool_security_tests.py
$(PYTHON) profiles/mhfu/tests/android_packaging_tests.py
$(PYTHON) -m unittest discover -s scripts/ci -p 'test_*.py'

java-test:
Expand Down
18 changes: 17 additions & 1 deletion docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,28 @@ cmake --build out/android-app -j 2

The first configure needs an SDL3 built for Android to find; the release script builds the pinned one (`packaging/linux/sources.sh`) into `out/release-android/deps` and points the directory at it. The overlay libraries are reused as they are as long as `include/psprecomp/` has not changed since they were built; the script refuses them otherwise, and when any is missing.

### PokeeFUC

A build directory configured with `-DMHFU_TITLE=FUC` as well (the FUComplete build; see the [profile README](../profiles/mhfu/README.md#fucomplete-work-in-progress)) packs **PokeeFUC** instead: application id `io.github.teamgdb.pokeefuc`, *PokeeFUC* on the launcher, `pokeefuc-<version>-android-arm64.apk`. It is another app, not an update of Pokee, so both install on one phone and neither sees the other's data. The Java code is the same; `build_apk.sh` gives the APK its id with `aapt2 link --rename-manifest-package` and its name with a copy of the resources. The corpus (`generated_fuc/`) and the overlays it compiles (`overlays_fuc/`) are generated on a computer first; the FUComplete steps in the profile README make them.

```sh
cmake -S . -B out/android-fuc -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$ANDROID_HOME/ndk/28.2.13676358/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29 -DCMAKE_BUILD_TYPE=Release \
-DPSPRECOMP_PROFILE=mhfu -DMHFU_TITLE=FUC -DMHFU_ANDROID_APP=ON \
-DSDL3_DIR=<SDL3 built for Android>/lib/cmake/SDL3 -DCMAKE_FIND_ROOT_PATH=<SDL3 built for Android>
cmake --build out/android-fuc -j 2
profiles/mhfu/scripts/release_android.sh out/android-fuc
```

The same key can sign both apps. The APK holds code recompiled from the game: a PokeeFUC APK built from your FUComplete image is for your own devices, not for publishing.

### What the script does

1. Fetches the pinned SDL3 and the fallback font, checks their SHA-256 and builds SDL3 for Android 10 once.
2. Reconfigures the build directory as a release (`MHFU_RELEASE`, no checkout paths, `android-29`) against that SDL3 and rebuilds `libmain.so` if that changed it.
3. Checks that every overlay library is there and newer than the framework headers.
4. Packs the APK with `packaging/android/build_apk.sh`: the native libraries, SDL's Java activity and the app's own, the font, and under `assets/licenses` the third-party notices, SDL's licence, FFmpeg's LGPL with where its source is, and the font's licence. The version name is `git describe`; the version code is the number of commits, so a later release installs as an update.
4. Packs the APK with `packaging/android/build_apk.sh`: the native libraries, SDL's Java activity and the app's own, the font, and under `assets/licenses` Pokee's licence, the third-party notices, the licences of Dear ImGui, tiny-AES-c and xxHash, SDL's licence, FFmpeg's LGPL with where its source is, and the font's licence. The keystore password reaches `apksigner` through the environment, not its command line. The version name is `git describe`; the version code is the number of commits, so a later release installs as an update.
5. Signs it with the release key, verifies the signature, refuses it if it holds any game file or if a library imports a function Android 10 lacks, and prints its size and SHA-256.

### The release key
Expand Down
2 changes: 2 additions & 0 deletions docs/SOURCE_PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Freedom Unite has one archive, so this port does not apply the manager's HD sele

## Profile code

The Android display frame rate request (`profiles/mhfu/host/platform/android_display.*`, its test, and `pacing::requested_rate`) and the safer argument, password and licence handling in `packaging/android/build_apk.sh` were ported from Yakumo's Android app at commit `5bacba3`, under the same MIT licence and by the same contributors; names were changed to this profile's.

A profile owns its generated AOT corpus, address-specific lowering, HLE behavior and native fast paths. Those files remain isolated under `profiles/<id>` so they do not become hidden dependencies of the generic framework.

## Analog camera
Expand Down
1 change: 1 addition & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- **Image conversion** (`mhfu_image_conversion_tests`) checks oversized 16-bit PNG conversion requests before allocation, freeing rejected input, and exact grayscale-to-RGBA output. Its allocator hook keeps synthetic oversized tests below one MiB.
- **Guest PCM staging** (`mhfu_guest_pcm_tests`) checks stereo/mono decoding, signed sample extrema, exact-end mapped buffers and rejection of overflowing or unmapped requests before allocation. It needs no audio device or game data.
- **Archive-tool security regressions** run with `python3 profiles/mhfu/tests/tool_security_tests.py` and in the desktop CI jobs. Synthetic ISO records and overlay names exercise traversal rejection, existing symlink escapes, malformed directory records and cycles, and overlay command argument handling; they use no game data.
- **Android packaging checks** run with `python3 profiles/mhfu/tests/android_packaging_tests.py` and in the Linux and macOS CI jobs. They run `packaging/android/build_apk.sh` against stand-in SDK tools and check which app a build packs (Pokee or PokeeFUC, from `MHFU_TITLE`), that an unknown title is refused, the bundled licences, the SDK version selection, and that the keystore password is passed through the environment. They need no SDK, NDK, game data or device, and do not show that a real APK installs or runs.
- **Framework tests** run with `ctest --test-dir out/framework` and need no game data.
- **Python lint CI** (`Python lint (Ruff)` in `.github/workflows/tooling.yml`) runs pinned Ruff correctness checks on all first-party Python scripts, tools and tests without game data or native builds. It fails on findings and excludes vendored/generated sources, game directories, overlays and build outputs. The same local command is documented in [CONTRIBUTING.md](../CONTRIBUTING.md#python-tooling); Python formatting is not enforced.
- **Unit-test CI** runs the framework and headless profile tests on standard GitHub-hosted Linux, macOS and Windows runners for pull requests, every pushed branch, and manual dispatch. The workflow in `.github/workflows/tests.yml` builds only `psprecomp_test_binaries` in Debug mode, then runs CTest with failure output and a per-test timeout. Failed jobs upload test logs and JUnit results when available. It uses no game data, generated game corpus, overlays or GPU. FFmpeg is disabled to avoid downloading and building application-only audio/video dependencies. The Vulkan descriptor-pool test is excluded from this renderer-disabled configuration. Android CI also cross-compiles the same targets with NDK 28.2 for `arm64-v8a` and `x86_64` at API 29 (Android 10). The x86_64 binaries run through ADB on a hardware-accelerated Android emulator, including the framework's synthetic code-generation checks. The runner deploys the shared C++ runtime, records each test's exit status and logs, applies a per-test timeout, and uploads diagnostics on failure. ARM64 is a compile check only. APK packaging, Java/JNI integration, Vulkan, gameplay and real Android devices still require separate testing, as does Steam Deck gameplay.
Expand Down
23 changes: 22 additions & 1 deletion profiles/mhfu/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ set(MHFU_PROGRAM_SOURCES
if(MHFU_ANDROID_APP)
add_library(Pokee SHARED ${MHFU_PROGRAM_SOURCES} host/platform/android_app.cpp
host/platform/android_jni.cpp host/platform/android_documents.cpp host/platform/android_performance.cpp
host/platform/android_fatal.cpp)
host/platform/android_fatal.cpp host/platform/android_display.cpp)
target_compile_definitions(Pokee PRIVATE MHFU_ANDROID_APP=1)
set_target_properties(Pokee PROPERTIES
OUTPUT_NAME main
Expand Down Expand Up @@ -1110,6 +1110,27 @@ if(PSPRECOMP_BUILD_TESTS)
add_test(NAME mhfu_render_tests COMMAND mhfu_render_tests)
endif()

# The Android app's display frame rate request (platform/android_display),
# against a stand-in libandroid.so: it is looked up at run time, so the tests
# need neither the NDK nor a device. One process per loader scenario, since
# the lookup happens once per process.
if(UNIX)
add_executable(mhfu_android_display_tests
tests/android_display_tests.cpp host/platform/android_display.cpp)
target_include_directories(mhfu_android_display_tests PRIVATE "${MHFU_PROFILE_DIR}/host")
target_compile_definitions(mhfu_android_display_tests PRIVATE
dlopen=pokee_test_dlopen dlsym=pokee_test_dlsym)
target_link_libraries(mhfu_android_display_tests PRIVATE ${CMAKE_DL_LIBS})
set_target_properties(mhfu_android_display_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
if(PSPRECOMP_BUILD_TESTS)
# Test names must be targets (psprecomp_test_binaries builds them).
foreach(scenario success missing_library missing_symbol rejected disabled)
add_custom_target(mhfu_android_display_${scenario} DEPENDS mhfu_android_display_tests)
add_test(NAME mhfu_android_display_${scenario} COMMAND mhfu_android_display_tests ${scenario})
endforeach()
endif()
endif()

# Frame interpolation checks: matching draws, cuts, blending, the present
# clock and the frame rate governor.
add_executable(mhfu_interpolation_tests
Expand Down
7 changes: 6 additions & 1 deletion profiles/mhfu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ What differs from the Freedom Unite build:
| Extras (analog camera, hold to gather, aspect ratio, HUD hiding, UI scaling, damage numbers, gather icon, sharpness gauge, empty item slot, give any rarity, lock-on, item box, layered armor) | on | on at FUComplete's own addresses (`host/title.hpp`), except the fast item box, which FUComplete already has |
| Mods page | the community's `DATA.BIN` mods | off: FUComplete has its own file replacer (`ms0:/PSP/SAVEDATA/FUCDAT/NATIVEPSP`) |

**On Android** the FUComplete build is a separate app, **PokeeFUC** (`io.github.teamgdb.pokeefuc`), so it installs beside Pokee with its own storage and saves. An Android build directory configured with `-DMHFU_TITLE=FUC` packs it: `release_android.sh` then writes `pokeefuc-<version>-android-arm64.apk`. The phone cannot generate the corpus, so generate it on a computer first, from `EBOOT.FUC.ELF` as above, and build any overlay dumps there too; the Android build compiles the `generated_fuc/` and `overlays_fuc/` it finds in the checkout. The app then asks for the same FUComplete image on its first start. Like the Linux copy, the APK holds code recompiled from your own game: keep it to yourself. See [RELEASING.md](../../docs/RELEASING.md#pokeefuc).

The scripts (`generate.sh`, `build_overlays.sh`, `bootstrap_overlays.sh`, `add_overlay.py`) read the title from the build directory they are given; `prepare_game.sh` reads `MHFU_TITLE` from the environment. FUComplete's own loader copies `PRELOAD.BIN` and `MODLOAD.BIN` to `ms0:/PSP/SAVEDATA/FUCDAT` on the first start and runs them from there; that code is not in the corpus and runs in the interpreter. FUComplete boots to its title, menus, character creation and quests on Linux.

The extras find the same code as in Freedom Unite at FUComplete's addresses: the executable's code sits a few words or 0x2FC lower (0x2948 higher in places), and game_task, lobby_task and game_sub load lower with their code. Each address has its FUComplete value beside Freedom Unite's, picked by the build's title, and each feature still checks the instructions it relies on before it acts, so a different FUComplete version leaves it off instead of writing to the wrong place. The data it reads moved as well: GameSys at 0x089C7508, the camera object at 0x089C6CB4, the entity manager at 0x09D146E0, and the HUD's textures 0x4400 lower (the item atlas at 0x091830A0). FUComplete's own name plate under the clock is 8 pixels shorter than Freedom Unite's, which UI Scaling takes into account.
Expand Down Expand Up @@ -457,7 +459,7 @@ The menu always selects with the bottom face button (A on an Xbox pad or a Steam

Every change applies at once and is saved to `settings.ini` in the per-user directory, next to the installer's `disc_image`. A setting whose environment variable is set is decided by that variable for the run: the menu shows it greyed with *Set by MHFU_…* and leaves the file's value alone. So the order is: environment variable, then `settings.ini`, then the default.

The Android app starts from other defaults where a phone differs, with the same keys and values: `video.aspect` is `fill` (a phone is wider than the PSP), `video.fullscreen` is on (there is no window) and `input.mouse` is off (a phone has no mouse to capture). Everything else starts as in the table. On Android, *Analog camera* alone decides whether a finger drag turns the camera: *Camera stick* is about a physical stick there.
The Android app starts from other defaults where a phone differs, with the same keys and values: `video.aspect` is `fill` (a phone is wider than the PSP), `video.hud_edges` is on (the quest interface goes out to the screen's sides), `video.fullscreen` is on (there is no window) and `input.mouse` is off (a phone has no mouse to capture). Everything else starts as in the table. On Android, *Analog camera* alone decides whether a finger drag turns the camera: *Camera stick* is about a physical stick there.

| Section | Setting | Key in `settings.ini` | Variable | Values |
| --- | --- | --- | --- | --- |
Expand All @@ -474,6 +476,7 @@ The Android app starts from other defaults where a phone differs, with the same
| Video | Frame rate | `video.frame_rate` | `MHFU_FRAME_RATE` | `30` (default: the game's own frames, as they are), `45`, `60`, `90`, `120` or `display` (the display's refresh rate): frames in between the game's, with blended movement. See [Frame rate](#frame-rate) |
| Video | Lower when behind | `video.frame_rate_auto` | `MHFU_FRAME_RATE_AUTO` | On (default): the frame rate steps down by itself rather than slow the game. Off: the chosen rate stays, and the game may run below full speed |
| Video | Hide HUD | | `MHFU_HIDE_HUD` | Off (default) or on, for this run: the game's HUD, name tags and prompts are left out of the picture; menus and dialogs stay. Same as the Hide HUD key. See [Hiding the HUD](#hiding-the-hud) |
| Video | Interface at the edges | `video.hud_edges` | `MHFU_HUD_EDGES` | Under `fill` on a screen wider than the PSP's, the quest interface goes out to the screen's sides: the clock, the bars and the party list to the left, the map and the item bar to the right. Off (the default; on for Android) keeps it in the PSP's proportions in the middle. Menus and names over hunters stay where the game draws them |
| Video | UI Scaling | `video.hud.map`, `video.hud.health`, `video.hud.item_bar`, `video.hud.names` | `MHFU_HUD_MAP`, `MHFU_HUD_HEALTH`, `MHFU_HUD_ITEM_BAR`, `MHFU_HUD_NAMES` | 50–150 each, the game's size as a percent; default 100. UI Scaling opens to Map, Status, Items, and Names, each with its own size. See [UI scaling](#ui-scaling) |
| Video | Performance | `video.performance` | `MHFU_PERF` | Off, overlay, overlay and log, log only, compact, compact and log. Compact is one line in the bottom-left corner: frames shown, the game's frame rate and its speed, green at full speed, yellow from 90%, red below |
| Video | GPU compatibility | `video.gpu_compat` | `MHFU_GPU_COMPAT` | Auto (default: on only when the start-up self-test fails or a pipeline is refused), on or off; applies from the next start. See [GPU compatibility mode](#gpu-compatibility-mode) |
Expand Down Expand Up @@ -993,6 +996,7 @@ The settings a player needs are in the [in-game menu](#in-game-menu). Environmen
| `MHFU_FAST_FORWARD` | `hold` | `toggle` makes the Fast-forward key a switch; `off` (or `0`) makes it do nothing (menu: Fast-forward). See [Fast-forward](#fast-forward) |
| `MHFU_FAST_FORWARD_SPEED` | `3` | Times real time while fast-forwarding, 2 to 8 (menu: Fast-forward speed) |
| `MHFU_FRAME_RATE` | `30` | `45`, `60`, `90`, `120` or `display`: present frames in between the game's 30 (menu: Frame rate). See [Frame rate](#frame-rate) |
| `MHFU_NO_DISPLAY_RATE_REQUEST` | off | Android: do not ask the system to run the display at the Frame rate setting's rate, as before. See [Frame rate](#frame-rate) |
| `MHFU_FRAME_RATE_AUTO` | on | `0` keeps the chosen frame rate even when the game falls behind (menu: Lower when behind) |
| `MHFU_NO_MATERIAL_COLOR` | off | Leave unlit geometry without vertex colours white instead of taking the material colour |
| `MHFU_NO_LIGHTING` | off | Draw lit geometry with the flat white stand-in used before lighting existed, and without fog, to compare a scene with and without them |
Expand Down Expand Up @@ -1085,6 +1089,7 @@ The game makes 30 frames a second, and its logic is tied to that rate: animation
- **30** shows the game's frames as they are, when the game flips them. This is the picture of earlier versions.
- **45**, **60**, **90** and **120** present that many frames a second: 1.5, 2, 3 and 4 per game frame. 45 is half of a 90 Hz screen (the Steam Deck's) and 90 all of it; 120 is for desktop monitors of 120 Hz or more.
- **Match display** presents at the display's refresh rate.
- On Android 11 and later the app asks the system to run the display at the chosen rate (Match display: the display's own), so a phone that switches between 60, 90 and 120 Hz can show 90 or 120. It is a wish, not a setting: the system may keep a lower rate to save power or when the phone is hot, and Android 10 has no way to ask. The log says `[render] requested Android surface frame rate`. `MHFU_NO_DISPLAY_RATE_REQUEST=1` leaves the display as it was.
- With **Vsync** on, the rate is never above the display's refresh: on a 60 Hz screen 90 and 120 run at 60. The row says so (*now 60*).
- If presenting that often would slow the game down, it steps down by itself to the fastest rate that fits (120, 90, 60, 45, 30), and tries a faster one again later once there is time to spare. It also steps down when the display has no image free for the presents (a screen that refreshes slower than the rate, such as a Steam Deck that gamescope switched to 60 Hz): a present waits at most 3 ms for one and is dropped otherwise. The row then shows *90 (running at 60)*. Loading screens and stalls while the game still has idle time do not count against it. **Lower when behind** (Video, on by default) turns this off: the chosen rate stays, capped only by Vsync, and the game may then run below full speed.
- The picture comes later than at 30: a frame is shown as it is one game frame after its own moment less the first in-between step, 16.7 ms later than at 30 at 60, 22.2 ms at 45 and 90, 25 ms at 120. The camera is still updated once per game frame. The pad is read when the game asks for it, at every Frame rate: the keyboard, mouse buttons and gamepad as they are then, not as they were at the last flip (issue #8), which takes up to a game frame off the input's own delay. `MHFU_PAD_AT_FLIP=1` reads it at the flip, as before.
Expand Down
20 changes: 20 additions & 0 deletions profiles/mhfu/host/gpu/frame_pacing.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "frame_pacing.hpp"

#include "settings/settings.hpp"

#include <algorithm>
#include <array>
#include <functional>
Expand All @@ -16,6 +18,24 @@ constexpr std::int64_t kGapFrames = 10;

} // namespace

double requested_rate(settings::FrameRate rate, double display_hz) noexcept {
switch (rate) {
case settings::FrameRate::Fps30:
return 30.0;
case settings::FrameRate::Fps45:
return 45.0;
case settings::FrameRate::Fps60:
return 60.0;
case settings::FrameRate::Fps90:
return 90.0;
case settings::FrameRate::Fps120:
return 120.0;
case settings::FrameRate::Display:
return std::isfinite(display_hz) && display_hz >= 1.0 ? std::max(30.0, display_hz) : 60.0;
}
return 30.0;
}

double presents_per_frame(double rate) noexcept {
const double presents = rate / 30.0;
// A display that reports 59.94 or 89.9 Hz means 2 or 3.
Expand Down
Loading
Loading