A profile is everything specific to one PSP title. It lives in the game's own repository, not here, and it should be a file of constants and a four-line CMakeLists.txt. If it has to be more than that, something belongs in the framework that is not there yet.
<game>/
CMakeLists.txt Four lines: add PortableKit, call portablekit_add_game()
portablekit/ The framework, as a submodule
host/<game>_profile.cpp The profile: one definition of portablekit::game()
config/nids.csv Optional: NIDs this game imports that the framework does not name
host/ Optional: per-game HLE and patches, reached through the profile's hooks
generated/ The AOT corpus, generated from the player's executable; never committed
overlays/ One directory per recompiled code overlay; never committed
game/ The player's own files; never committed
docs/ Compatibility, testing and release pages for this port
Nothing derived from the game belongs in the repository: no disc image, no executable, no generated code, no captures of the game's own art, no saves.
cmake_minimum_required(VERSION 3.20)
project(Tenkawa VERSION 0.1.0 LANGUAGES CXX)
add_subdirectory(portablekit)
portablekit_add_game(TenkawaNative
PROFILE_DIR "${CMAKE_CURRENT_SOURCE_DIR}"
SOURCES host/tenkawa_profile.cpp)portablekit_add_game() does the rest: the corpus, the renderer and its shaders, the version header, FFmpeg, the NID table, the overlay modules, the stack size and the output directory. SOURCES are the profile's own host sources, relative to PROFILE_DIR; one of them must define portablekit::game().
portablekit::GameProfile in host/profile.hpp is the whole interface, and its comments say what each field is for. The rule for what belongs in it: a value the framework needs but cannot derive from the executable it was given. Anything the ELF already says — its entry point, its segments, its imports — is read from the ELF, not declared.
Read the values off the player's own disc rather than recalling them:
| Field | Where it comes from |
|---|---|
disc_id, game_title |
PSP_GAME/PARAM.SFO |
encrypted_executable_sha256 |
sha256 of PSP_GAME/SYSDIR/EBOOT.BIN |
executable_sha256 |
sha256 of what the installer decrypts it to |
decryption_tag |
The word at offset 0xD0 of EBOOT.BIN |
decryption_key |
The published key table's entry for that tag |
decryption_key_table |
Instead of decryption_key, for a tag that selects the older header layout: the published 0x90-byte table for that tag. Where the disc also carries an unencrypted PSP_GAME/SYSDIR/BOOT.BIN, the decrypted result can be compared with it byte for byte, which is how to check the table |
load_base, guest_ram_bytes |
The ELF's program headers |
overlay_slots |
The executable's section table, inside the load image's BSS |
save_game_name, save_folders |
The paths the executable itself contains |
release_name and other_releases are for the installer's refusal: the first names the supported release ("the Japanese release"), the second gives the sentence to add for a release a player is likely to have instead. Yakumo tells a player with the original PSP release, ULJM05800, that it is not the HD release.
Two fields are hooks rather than values. register_extra_hle adds calls this game makes that the framework does not implement, or replaces one it gets wrong for this game. patch_loaded_image is for per-game fixes with no better home. Both may be null, and a profile that needs neither is the goal.
Two more are for a port that goes further than the PSP did, and both are null until a game has earned them:
camera, aCameraDriver: drives the game's own camera from what the player asks of it. The framework turns every device — the second stick, camera keys, the mouse — into one request (host/camera/camera_input.hpp); the driver takes it at each flip and says whether the game must stop acting on the second stick meanwhile, whether the game is aiming, and how fast the current camera turns (host/camera/camera_driver.hpphas the framework's side and its defaults). Where a game keeps its camera is the game's, so the driver is too. Without one, Analog camera and its rows are not offered and what the devices ask of the camera is dropped. Yakumo'shost/camera/game_camera.cppis the example.view_aspect_frame: called at each flip with the shape of the picture the game is drawn into, so a game that can widen its own view does. Without it the renderer does not offer Fill. Yakumo'shost/camera/game_aspect.cppis the example.
trigger_profiles are for how a game is played rather than how it is run: what the triggers may press instead of L and R, each with the key settings.ini keeps and the label the menu shows. Yakumo offers Bows (R on L2 to hold the aim, △ on R2 to shoot) and Bowguns (R and ○). Without any, the triggers press L and R and the menu has no row for them.
tilt is for a game played by tilting its world, as LocoRoco is with L and R: the buttons that tilt left and right, whether the game eases its tilt towards the held side (then a small roll of the device presses the button for part of the frames and tilts the world part of the way), and a sentence for the menu. With it, Controls gets a Tilt controls section, off by default, and the motion sensors of a gamepad (DualShock 4, DualSense, Switch Pro, a Steam Deck seen through SDL's own driver) or of the phone itself press those buttons (host/input/tilt.hpp for the arithmetic, host/input/motion.hpp for the sensors). Find the buttons and the easing with the game's pad trace before filling it in. Without it, nothing opens a sensor and the menu has no such section.
player_name_label and default_player_name are what the interface calls the name a game asks for and what it answers before the player sets one: "Player name" and "Player" unless a profile says otherwise. Yakumo says "Hunter name" and "Hunter".
glyph_cache is for changing the font while a game runs. A game that caches the glyphs it has drawn keeps them until its atlas needs the cell, so a new font shows only in text it has not drawn yet. Where the game keeps that atlas, read off its own text code, lets the framework make it draw them again. Null for any game nobody has traced.
interpolation_thresholds is for frame interpolation, which blends the game's frames and must not blend across a camera cut. The angles and fractions it uses to tell a cut from motion hold for any game; its distances are in the game's own world units, and the defaults were measured on Monster Hunter Portable 3rd. A game whose world is on a very different scale gives its own; one that does not, gets those defaults.
A driver writes into the game's memory, so check the code it relies on before it writes anything — Yakumo compares every instruction and constant it depends on at start-up, from patch_loaded_image, and stays out, saying why, if one differs.
Recompiled code is a translation of the game's own executable, so it is derived from copyrighted material. Every player generates it from their own copy. Make that reproducible by documenting the executable identity and hash the profile expects, the exact generator command, and any deterministic pass that follows it.
When the port needs a change, ask which repository it belongs in.
- It names the game, its addresses, its folders or its behaviour — the profile.
- It is true of the PSP, of a file format, or of any game that makes the same call — the framework.
A profile that grows its own kernel, its own renderer changes or its own copies of framework code has found a missing seam. Add the seam instead.