├── .github └── workflows │ └── build.yml ├── CMakeLists.txt ├── README.md ├── dump_runner.cpp └── homebrew.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: dump_runner 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "**/*.md" 7 | - "**/*.txt" 8 | pull_request: 9 | paths-ignore: 10 | - "**/*.md" 11 | - "**/*.txt" 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: ${{ github.ref }}-${{ github.event_name }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | payload-build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v3 25 | 26 | - name: Install dependencies 27 | run: | 28 | sudo apt update 29 | sudo apt install -y build-essential clang-18 lld-18 xxd yasm nasm 30 | 31 | - name: Install toolchain 32 | run: | 33 | wget https://github.com/ps5-payload-dev/pacbrew-repo/releases/latest/download/ps5-payload-dev.tar.gz 34 | sudo tar xf ps5-payload-dev.tar.gz -C / 35 | 36 | - name: Build Payload 37 | run: | 38 | export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk 39 | /opt/ps5-payload-sdk/bin/prospero-cmake CMakeLists.txt 40 | make 41 | 42 | - name: Upload Payload artifact 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: dump_runner 46 | path: | 47 | ./dump_runner.elf 48 | ./homebrew.js 49 | if-no-files-found: error 50 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(dump_runner CXX) 4 | 5 | add_executable(dump_runner dump_runner.cpp) 6 | target_link_libraries(dump_runner PRIVATE SceSystemService SceUserService) 7 | set_target_properties(dump_runner PROPERTIES OUTPUT_NAME dump_runner.elf) 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Homebrew Dump Runner 2 | 3 | Run homebrew games on your PS5 using the Kstuff, Websrv payloads and Homebrew Launcher. 4 | 5 | --- 6 | 7 | ### ✅ Installation & Launch 8 | 9 | 1. **Copy your homebrew folder** to: 10 | - `/data/homebrew/` 11 | - `/mnt/usb#/homebrew/` *(replace `#` with your USB number, e.g., `usb0`, `usb1`, etc.)* 12 | - `/mnt/ext#/homebrew/` *(replace `#` with your EXT number, e.g., `ext0`, `ext1`, etc.)* 13 | 14 | 2. **Inside each homebrew game folder**, add: 15 | - `dump_runner.elf` 16 | - `homebrew.js` 17 | 18 | 3. **Install the Homebrew Launcher** and send the Websrv payload to your console: 19 | 👉 [Websrv v0.23.1](https://github.com/ps5-payload-dev/websrv/releases/tag/v0.23.1) 20 | 21 | 4. **Open the Homebrew Launcher.** 22 | With the USB plugged in, your homebrew game should appear. 23 | **Select a homebrew game and run it.** 24 | 25 | --- 26 | 27 | ### 📌 Example Folder Structure 28 | 29 | `/data/homebrew/MyHomebrewGame/`, `/mnt/usb0/homebrew/MyHomebrewGame/` or `/mnt/ext0/homebrew/MyHomebrewGame/` 30 | - `dump_runner.elf` 31 | - `homebrew.js` 32 | - `[other homebrew game files...]` 33 | 34 | --- 35 | -------------------------------------------------------------------------------- /dump_runner.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #define IOVEC_ENTRY(x) {x ? (char *)x : 0, x ? strlen(x) + 1 : 0} 11 | #define IOVEC_SIZE(x) (sizeof(x) / sizeof(struct iovec)) 12 | 13 | typedef struct app_launch_ctx 14 | { 15 | uint32_t structsize; 16 | uint32_t user_id; 17 | uint32_t app_opt; 18 | uint64_t crash_report; 19 | uint32_t check_flag; 20 | } app_launch_ctx_t; 21 | 22 | extern "C" 23 | { 24 | int sceUserServiceInitialize(void *); 25 | int sceUserServiceGetForegroundUser(uint32_t *); 26 | void sceUserServiceTerminate(void); 27 | int sceSystemServiceLaunchApp(const char *, char **, app_launch_ctx_t *); 28 | } 29 | 30 | int remount_system_ex(void) 31 | { 32 | struct iovec iov[] = { 33 | IOVEC_ENTRY("from"), 34 | IOVEC_ENTRY("/dev/ssd0.system_ex"), 35 | IOVEC_ENTRY("fspath"), 36 | IOVEC_ENTRY("/system_ex"), 37 | IOVEC_ENTRY("fstype"), 38 | IOVEC_ENTRY("exfatfs"), 39 | IOVEC_ENTRY("large"), 40 | IOVEC_ENTRY("yes"), 41 | IOVEC_ENTRY("timezone"), 42 | IOVEC_ENTRY("static"), 43 | IOVEC_ENTRY("async"), 44 | IOVEC_ENTRY(NULL), 45 | IOVEC_ENTRY("ignoreacl"), 46 | IOVEC_ENTRY(NULL), 47 | }; 48 | 49 | return nmount(iov, IOVEC_SIZE(iov), MNT_UPDATE); 50 | } 51 | 52 | int mount_nullfs(const char *src, const char *dst) 53 | { 54 | struct iovec iov[] = { 55 | IOVEC_ENTRY("fstype"), 56 | IOVEC_ENTRY("nullfs"), 57 | IOVEC_ENTRY("from"), 58 | IOVEC_ENTRY(src), 59 | IOVEC_ENTRY("fspath"), 60 | IOVEC_ENTRY(dst), 61 | }; 62 | 63 | return nmount(iov, IOVEC_SIZE(iov), 0); 64 | } 65 | 66 | int endswith(const char *string, const char *suffix) 67 | { 68 | size_t suffix_len = strlen(suffix); 69 | size_t string_len = strlen(string); 70 | 71 | if (string_len < suffix_len) 72 | { 73 | return 0; 74 | } 75 | 76 | return strncmp(string + string_len - suffix_len, suffix, suffix_len) != 0; 77 | } 78 | 79 | int chmod_bins(const char *path) 80 | { 81 | char buf[PATH_MAX + 1]; 82 | struct dirent *entry; 83 | struct stat st; 84 | DIR *dir; 85 | 86 | if (stat(path, &st) != 0) 87 | { 88 | return -1; 89 | } 90 | 91 | if (endswith(path, ".prx") || endswith(path, ".sprx") || endswith(path, "/eboot.bin")) 92 | { 93 | chmod(path, 0755); 94 | } 95 | 96 | if (S_ISDIR(st.st_mode)) 97 | { 98 | dir = opendir(path); 99 | while (1) 100 | { 101 | entry = readdir(dir); 102 | if (entry == nullptr) 103 | { 104 | break; 105 | } 106 | 107 | if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) 108 | { 109 | continue; 110 | } 111 | 112 | sprintf(buf, "%s/%s", path, entry->d_name); 113 | chmod_bins(buf); 114 | } 115 | 116 | closedir(dir); 117 | } 118 | 119 | return 0; 120 | } 121 | 122 | int main(int argc, char *argv[]) 123 | { 124 | app_launch_ctx_t ctx = {0}; 125 | char src[PATH_MAX + 1]; 126 | char dst[PATH_MAX + 1]; 127 | const char *title_id; 128 | 129 | if (argc < 2) 130 | { 131 | printf("Usage: %s TITLE_ID\n", argv[0]); 132 | return -1; 133 | } 134 | 135 | title_id = argv[1]; 136 | getcwd(src, PATH_MAX); 137 | 138 | strcpy(dst, "/system_ex/app/"); 139 | strcat(dst, title_id); 140 | 141 | sceUserServiceInitialize(0); 142 | sceUserServiceGetForegroundUser(&ctx.user_id); 143 | 144 | if (access(dst, F_OK) != 0) 145 | { 146 | remount_system_ex(); 147 | mkdir(dst, 0755); 148 | } 149 | 150 | mount_nullfs(src, dst); 151 | chmod_bins(src); 152 | 153 | return sceSystemServiceLaunchApp(title_id, &argv[2], &ctx); 154 | } 155 | -------------------------------------------------------------------------------- /homebrew.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const CWD = window.workingDir 3 | const PAYLOAD = CWD + '/dump_runner.elf'; 4 | const PARAM_URL = baseURL + '/fs/' + CWD + '/sce_sys/param.json'; 5 | 6 | const resp = await fetch(PARAM_URL); 7 | const param = await resp.json(); 8 | 9 | var name = ''; 10 | for(const key in param.localizedParameters) { 11 | if(key.startsWith('en-')) { 12 | name = param.localizedParameters[key]['titleName']; 13 | break; 14 | } 15 | } 16 | 17 | return { 18 | mainText: name, 19 | secondaryText: param['titleId'], 20 | onclick: async () => { 21 | return { 22 | path: PAYLOAD, 23 | cwd: CWD, 24 | args: [PAYLOAD, param['titleId']], 25 | daemon: true, 26 | }; 27 | } 28 | }; 29 | } 30 | --------------------------------------------------------------------------------