Directory listing skips per-entry getxattr and .holes probes; Engage's first frame at 1.8 s on the Mac
- Engage lists all of RomFS (216,554 entries) before its first frame. ReadDirectory called stat, getxattr (guest name) and a failing fopen("<file>.holes") for every entry, about 5 s on the Mac and 10 s on an iPhone (device log)
- guest_name reads the xattr only for .dm-case-* physical names: set_guest_name writes it only when the physical name differs from the guest name, which only those collision names do
- Directories take their type from d_type; files use fstatat and the .holes probe uses openat, both relative to the directory fd instead of resolving the full path again
- First frame 5.4 → 1.8 s on the Mac (three runs: 2.1 / 1.8 / 1.8 s under another app's load), opening movie at 7.4 s; Engage save → battle run clean, DLC still mounted; t_fs 68/68, t_fs_case 51/51; Mario regression t83 clean
This commit is contained in:
1 parent
5c7ac33752
commit
145cb36b7f
2 files changed
+20
-6
No files matched your search
+19
-5
@@ -138,6 +138,11 @@ static Result mount(const char *name, const char *dir, int ro) { return mount_ex
|
||||
// clonefile preserves xattr, so save Commit/remount needs no separate mapping table; enumeration must not leak physical aliases.
|
||||
#define FS_GUEST_NAME "org.d2rmobile.guest-name"
|
||||
static int guest_name(const char *parent, const char *physical, char *out, size_t n) {
|
||||
// 只有大小写冲突时另取的物理名(.dm-case-N)才带 xattr(set_guest_name 只在物理名 ≠ 客体名时写);别的物理名就是客体名,不必逐项 getxattr——
|
||||
// 列目录时每项一次系统调用,Engage 开机遍历 RomFS 的 21.6 万项要几秒
|
||||
// Only the physical names chosen on a case collision (.dm-case-N) carry the xattr (set_guest_name writes it only when the physical name differs from the guest name); any other physical name is the guest name,
|
||||
// so no per-entry getxattr is needed -- that was one system call per entry when listing, seconds for the 216,000 entries Engage walks in RomFS at boot
|
||||
if (strncmp(physical, ".dm-case-", 9)) { strlcpy(out, physical, n); return 1; }
|
||||
char p[1280]; snprintf(p, sizeof p, "%s/%s", parent, physical);
|
||||
ssize_t k = getxattr(p, FS_GUEST_NAME, out, n - 1, 0, XATTR_NOFOLLOW);
|
||||
if (k >= 0) { out[k] = 0; return 1; }
|
||||
@@ -588,6 +593,13 @@ void dm_data_note_fd(int fd, const char *hostpath) { // 打开资源档案后
|
||||
if (fd < 0 || fd >= 4096) return;
|
||||
g_hm[fd] = hm_load(hostpath);
|
||||
}
|
||||
// 同 dm_data_logical_size,但按目录 fd 相对打开:列目录时不必每项再从根解析一遍长路径 | Same as dm_data_logical_size, but opened relative to the directory fd: listing does not resolve the long path from the root again for every entry
|
||||
static int64_t data_logical_size_at(int dfd, const char *name, int64_t phys_size) {
|
||||
char hn[0x320]; if (snprintf(hn, sizeof hn, "%s.holes", name) >= (int)sizeof hn) return phys_size;
|
||||
int fd = openat(dfd, hn, O_RDONLY); if (fd < 0) return phys_size;
|
||||
FILE *f = fdopen(fd, "r"); if (!f) { close(fd); return phys_size; }
|
||||
long long size = -1; int ok = fscanf(f, "size %lld", &size) == 1; fclose(f); return ok && size >= 0 ? size : phys_size;
|
||||
}
|
||||
int64_t dm_data_logical_size(const char *hostpath, int64_t phys_size) { // 列目录 / 取大小用:有区段表报原始大小 | For directory listing / size queries: report the original size when there is an extent table
|
||||
char hp2[1100]; if (snprintf(hp2, sizeof hp2, "%s.holes", hostpath) >= (int)sizeof hp2) return phys_size;
|
||||
FILE *f = fopen(hp2, "r"); if (!f) return phys_size; long long size = -1; int ok = fscanf(f, "size %lld", &size) == 1; fclose(f); return ok && size >= 0 ? size : phys_size;
|
||||
@@ -802,16 +814,18 @@ HLE_FORCE("_ZN2nn2fs13ReadDirectoryEPlPNS0_14DirectoryEntryENS0_15DirectoryHandl
|
||||
if (!hh) { *count = 0; return RES_FAIL; }
|
||||
PATH_LOCK(); dirh_t *h = (dirh_t *)hh; int64_t n = 0; struct dirent *e;
|
||||
if (h->snap) { while (n < max && h->psnap < h->nsnap) { DirectoryEntry *src = &h->snap[h->psnap++]; if (!(h->mode & (src->type == ENT_DIR ? DIRMODE_DIR : DIRMODE_FILE))) continue; ents[n++] = *src; } *count = n; return RES_OK; }
|
||||
int dfd = dirfd(h->d);
|
||||
while (n < max && (e = readdir(h->d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
{ size_t dl = strlen(e->d_name); if (dl > 6 && !strcmp(e->d_name + dl - 6, ".holes")) continue; } // 区段表不给游戏看 | The extent table is not shown to the game
|
||||
char c[1280]; snprintf(c, sizeof c, "%s/%s", h->path, e->d_name);
|
||||
struct stat st; if (stat(c, &st)) continue;
|
||||
int isdir = S_ISDIR(st.st_mode);
|
||||
// 目录的类型直接取 d_type,不 stat;文件要大小,按目录 fd 相对 fstatat(跟随链接,同原来的 stat) | a directory's type comes from d_type without stat; files need a size, taken with fstatat relative to the directory fd (following links, like the old stat)
|
||||
struct stat st; int isdir;
|
||||
if (e->d_type == DT_DIR) isdir = 1;
|
||||
else { if (fstatat(dfd, e->d_name, &st, 0)) continue; isdir = S_ISDIR(st.st_mode); }
|
||||
if (!(h->mode & (isdir ? DIRMODE_DIR : DIRMODE_FILE))) continue;
|
||||
DirectoryEntry *o = &ents[n++]; memset(o, 0, sizeof *o);
|
||||
if (!guest_name(h->path, e->d_name, o->name, sizeof o->name)) { *count = n - 1; return err_res("directory name", c); }
|
||||
o->type = isdir ? ENT_DIR : ENT_FILE; o->size = isdir ? 0 : dm_data_logical_size(c, st.st_size);
|
||||
if (!guest_name(h->path, e->d_name, o->name, sizeof o->name)) { char c[1280]; snprintf(c, sizeof c, "%s/%s", h->path, e->d_name); *count = n - 1; return err_res("directory name", c); }
|
||||
o->type = isdir ? ENT_DIR : ENT_FILE; o->size = isdir ? 0 : data_logical_size_at(dfd, e->d_name, st.st_size);
|
||||
}
|
||||
*count = n; return RES_OK;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
|
||||
- **Abort in NVIDIA's video path (`NvRmChannelOpen` → `NvOsDrvOpen`)** → the SDK movie library's hardware decoder opens nvdrv directly. The host takes over `movie::Decoder` (`hle_movie.c`, next entry); nvdrv opens fail (`hle_nvdrv.c`).
|
||||
- **The game waits forever at a movie (Engage's 94 s opening `S01F.webm`; frames keep presenting, nothing happens)** → Unity demuxes the WebM itself and feeds VP9 packets to `movie::Decoder`; with no working decoder the movie never ends. `movie::Decoder` is a thin wrapper over Android's MediaCodec, and its vtable entries are symbol relocations, so `HLE_FORCE` on each method also catches Unity's virtual calls. The host implements the protocol: four `nn::os` events (`DecoderEvents` = input available, output ready, format changed, error), input indices (`AcquireInputBufferIndex`, none = 0x1011; `GetInputBuffer` fills the client's `movie::Buffer`; `SendInputBufferForDecode(index, offset, size, µs, flags)`), output indices (`AcquireOutputBufferIndex`, none = 0x100e; one NV12 frame per packet copied into the buffer Unity allocated, w × h × 1.5; `ReleaseOutputBufferIndex`). While every output slot is taken, packets wait and their input indices are not returned, as with MediaCodec; dropping them instead lost frames. `Buffer`, `MediaData` and events are handled through the SDK's own exported functions (`SetDataAndCapacity`, `SetRange`, `SetInt32`, `SetRect`, `SignalEvent`). Unity reads `width`, `height`, `crop`, `nv12-y-stride`, `nv12-y-offset`, `nv12-uv-offset` and `slice-height`. VP9 packets are decoded with libvpx (`dm/third_party/libvpx`) on one host thread per decoder: guest threads run on the stacks the SDK gives them, too small for libvpx. The guest thread that sends a packet or returns an output index waits for the decode and signals the events itself, because `SignalEvent` is guest code and cannot run on a host thread. Packets are decoded in arrival order from one queue. When the movie ends the game continues to its title screen.
|
||||
- **Null read in `sdk` under `nn::applet::CreateLibraryApplet` (Engage: title menu → 新增内容 → `nn::ec::ShowShopAddOnContentList`)** → every `Show…` call that opens a system applet (eShop, friend lists, web pages, error screens) goes through the applet proxy that `oe::Initialize` would have set up, and the host has none. Hook each `Show…` overload the game imports: list `main`'s undefined symbols containing `Show` / `Applet` and grep the host sources for each mangled name; one overload missing is enough to crash. The shop calls return `void` and act as if the player closed the shop (`hle_misc.c`); the friends and web ones return failure (`hle_friends.c`, `hle_offline.c`).
|
||||
- **A long black screen at boot with nothing in the log (Engage: about 17 s on the Mac, 32 s on an iPhone)** → sample the process (`sample <pid> 3`). If the top of stack is `getxattr` under `path_component`, the game is walking RomFS, and every path component scanned its whole parent directory. That scan maps the game's case-sensitive names onto APFS, so it calls `getxattr` on each entry, and Engage has directories of 38,000 files. Read-only mounts now use a per-directory name index built at the first lookup (`dir_index_get` in `hle_fs.c`), which also speeds up every file Wwise opens during play. The first frame now comes at 5.7 s on the Mac, and the opening movie starts at 11 s instead of 49 s.
|
||||
- **A long black screen at boot with nothing in the log (Engage: about 17 s on the Mac, 32 s on an iPhone)** → sample the process (`sample <pid> 3`). If the top of stack is `getxattr` under `path_component`, the game is walking RomFS, and every path component scanned its whole parent directory. That scan maps the game's case-sensitive names onto APFS, so it calls `getxattr` on each entry, and Engage has directories of 38,000 files. Read-only mounts now use a per-directory name index built at the first lookup (`dir_index_get` in `hle_fs.c`), which also speeds up every file Wwise opens during play. The first frame then came at 5.7 s on the Mac. The rest was the listing itself: `ReadDirectory` called `stat`, `getxattr` and a failing `fopen("<file>.holes")` for every entry. Now only `.dm-case-*` names (the only ones that carry the xattr) read it, directories take their type from `d_type`, and files use `fstatat` / `openat` relative to the directory. The first frame comes at 1.8 s on the Mac (9.9 s → expected about 3 s on an iPhone), and the opening movie starts at 7 s instead of 49 s.
|
||||
- **DLC the player owns is not unlocked** → the host reports add-on content from `DM_AOC_DIR`, one numbered folder per DLC index (title ID − base − 0x1000) holding that DLC's RomFS. `build_ipa_from_nsp.py` extracts the DLC NSPs in the folder with `nx_extract --aoc`, `build_ipa.sh` copies them to `<App>/aoc/`, and the app sets `DM_AOC_DIR`. `aoc::CountAddOnContent` / `ListAddOnContent` report those indices (`hle_misc.c`), and `fs::MountAddOnContent` mounts the folder read-only (`hle_fs.c`). Engage's two DLC packages each hold one empty marker file (`e0`, `h0`), and the content is already in update 2.0.0. `DLCManager.RefreshHasList` marks a content owned when its mount succeeded (probe `main+0x29f3c88`). D2R keeps its fixed `{1}` without `DM_AOC_DIR`; `DM_AOC_OWNED=0` reports none.
|
||||
- **Abort in `nn::sm::Initialize` under `nn::pctl::IsFreeCommunicationAvailable` (Engage, entering its first battle map)** → the SDK connects to the parental-control service through `sm`. The host answers it like the other free-communication calls: not available (`hle_misc.c`).
|
||||
- **Abort inside `nn::sm::Initialize`** → an SDK path reached a system service the host doesn't replace → hook the public SDK entry point named in the backtrace (`HLE_FORCE` in the relevant `hle_*.c`). The log shows it as `SVC 0x1f` (ConnectToNamedPort, port name `sm:`) and `SVC 0x21` answered by placeholders, then `nn::diag` abort.
|
||||
|
||||
Reference in new issue
Block a user