Repository navigation
Conversation
- Add missing typename keywords to dependent type constructors (DataT, NodePtrT, DeleterT, WeakPtrT) in TreeContainer. - Disambiguate UnicodeString assignment from LPCWSTR in applyReroute by constructing UnicodeString explicitly. - Cast unsigned hex status literals (0xC0000022, 0xC0000035) to signed NTSTATUS in std::formatter<usvfs::log::Wrap<NTSTATUS>> switch cases to avoid narrowing conversions. - Remove quotes around pch.h in the /FI compile option in shared/CMakeLists.txt.
- Add DirectoryQuery to normalize both entry points, folding the legacy ReturnSingleEntry and RestartScan arguments into SL_* flags. - Add queryNative to forward a query through its original entry point. - Move the duplicated query body into queryDirectory; both hooks now only build a DirectoryQuery and route it. - Log QueryFlags for both entry points while keeping the hook name in the log.
- Move HandleTracker into handle_tracker.h and declare ntdllHandleTracker extern so other hook sources can use it. - Move DirectoryQuery, queryNative, queryDirectory and the search helpers into directory_scan.h/.cpp without changing their logic. - Add eraseDirectorySearch so NtClose no longer touches Searches directly, keeping the SearchInfo data ID in a single translation unit. - Drop includes from ntdll.cpp that only the moved code needed.
- Move the NtQueryDirectoryFile(Ex) hook tests out of tvfs_test/main.cpp into a dedicated directory_enumeration.cpp. - Share REAL_FILEA/W, defaultUsvfsParams, the USVFSTest fixture and hooked_NtOpenFile through tvfs_test.h, made inline for both sources.
- Pass FileName->Length to string_cast when building the virtual search pattern, instead of reading the buffer up to the next NUL. - Add a test querying a virtual file through a pattern whose buffer continues past Length.
- Add shouldVirtualize so queryDirectory only virtualizes the directory information classes whose records carry a FileName. - Forward every other class to ntdll, which reports its own status instead of STATUS_NO_SUCH_FILE. - Add a test comparing the hook with a direct ntdll call for a non-directory information class.
- Replace the Searches custom data with a DirectoryScans registry mapping handles to shared DirectoryScan objects under its own shared_mutex. - Rename Searches::Info to DirectoryScan and drop the SearchInfo data ID along with the unused query mutex. - Erase scans in NtClose outside the hook context lock, keeping the manual handle close where it was.
- Move the first-query block (SearchHandleMap lookup, reopen and gatherVirtualEntries) into the DirectoryScan constructor. - Add DirectoryScans::scanFor, which constructs scans outside the registry lock and swaps in a new scan on restart instead of erasing the old one first. - Track the first call in DirectoryScan::isFirstCall instead of inferring it from a missing registry entry.
- Move the regular and virtual phases and the status computation from queryDirectory into DirectoryScan::query. - Reduce queryDirectory to shouldVirtualize followed by scanFor and query. - Make the scan state private and turn gatherVirtualEntries and addVirtualSearchResult into private members. - Add DirectoryScan::closeSearchHandle for NtClose and the end of the regular phase. - Drop the dead searchPattern field.
- Move the regular and virtual phases into DirectoryScan::fill, which returns STATUS_SUCCESS when it wrote entries and STATUS_NO_MORE_FILES once both phases are exhausted. - Keep the first-call status mapping, the status block and the logging in DirectoryScan::query.
- Turn gatherVirtualEntries back into a free function that returns a vector of VirtualMatch without touching scan state. - Move VirtualMatch out of DirectoryScan to file scope. - Fill the shadowed names from the returned matches in the DirectoryScan constructor. - Replace the virtual match queue with a vector and a cursor.
- Add openRegularSource, which reopens the search path stored for a handle and returns INVALID_HANDLE_VALUE when there is none or the open fails. - Look up SearchHandleMap under the single context lock, since the stored path also names the virtual directory. - Call CreateFileW after releasing the context lock, as the last step of the DirectoryScan constructor.
- Add CaseInsensitiveNameSet with insert and clear, still backed by a std::set of to_upper keys. - Use it for the found names of DirectoryScan and in addNtSearchData instead of uppercasing at each call site.
- Add OwnedHandle, a move-only handle that closes itself when destroyed, and use it for the scan's private directory handle. - Close the handle of a scan replaced by a restart or dropped by NtClose when the scan is destroyed instead of leaking it. - Remove the manual closes, including the one on INVALID_HANDLE_VALUE after a failed open. - Move the handle validity check out of HandleTracker into the free function isValidHandle. - Add a test that restarting a query keeps the process handle count unchanged.
- Guard the state of a DirectoryScan with a mutex held for the whole query, as ntdll serializes queries on a synchronous handle. - Add a test that queries one handle from eight threads at once and expects every entry exactly once.
- Leave queries that ask for an event, an APC or a completion packet to ntdll, since the virtual listing completes them itself and delivers none of these notifications. - Forward every query on an asynchronous handle to ntdll through a passthrough scan, decided once per scan from FileModeInformation. - Stop handing the caller's event and APC to the internal directory reads, which notified the caller of the wrong read and could complete into a gone stack frame. - Note in a TODO what a full emulation of the completion would take. - Add tests that compare the event, APC and completion packet notifications of every query with those ntdll delivers.
- Report no bytes once a listing has ended, instead of the buffer length after the last entry. - Leave the status block untouched when the first query finds nothing, as ntdll does for failed queries. - Add tests that compare the status block of both cases with what ntdll writes.
- Leave queries whose buffer cannot hold a record, or is misaligned, to ntdll, which rejects them before reading the directory. - Describe the record size and alignment of each directory information class in a DirectoryRecordLayout table that replaces the switch in shouldVirtualize. - Require the alignment of the record on x64, 4 bytes for the Ex entry point under WOW64, and none for the legacy one, which wow64.dll realigns. - Add tests that compare the result for undersized and misaligned buffers with what ntdll returns.
- Record the offsets of FileNameLength and FileName in the layout of each directory information class. - Add DirectoryRecord, which reads the fields of a record in place and reports a record cut short by the end of the data as truncated. - Add unit tests on synthetic records of every class, and check the names it reads against those FindFirstFileW lists on the system volume and on the volume the tests run from.
- Read the records in addNtSearchData through DirectoryRecord, and rename them and end their list with local helpers that keep the stock behaviour. - Record where the 8.3 name sits in the layout of the classes that carry one. - Delete file_information_utils.h, which nothing else used.
- Add DirectoryOutput, which lays out records 8 bytes apart and links them like ntdll. - Report the size up to the end of the last record, without trailing padding. - Fit a record after the first by its unpadded size, as NTFS and ReFS do. - Keep the stock short-name rewrite in appendRenamed until 8.3 names are dropped. - Turn the fill status into the query result and write the status block unless it is an error. - Check the layout and the completions against native listings on the system volume and the test volume.
- Generalize the native listing helpers to list through any query function. - List directories without virtual entries through ntdll and the hook in every class, and compare the result, status block and records of each query.
- Add RegularEntryReader, which reads regular entries into a staging area and writes those no virtual entry shadows. - Hold it in an optional whose reset ends the regular phase and releases its handle and staging area. - End the data of a query with its last regular record, without the padding after it. - Keep the stock behavior of ending the regular phase on any result but success. - Check that a query whose shadowed records follow ".." ends with "..".
- Compare the virtual file name in NtQueryDirectoryFileFindsVirtualFile by FileNameLength, since FileName is not terminated.
- Add VirtualEntryReader, which reads each virtual entry from the directory its file lies in and writes it under its virtual name. - Open that directory for a single native read per entry and close it right after. - Write the status block through DirectoryOutput::complete and remove addNtSearchData and addVirtualSearchResult. - Report a renamed record up to the end of its virtual name. - Check a virtual entry against the record ntdll lists for its file.
- Log one line per scan with the virtual path, the pattern, the regular source and the number of virtual entries. - Drop the per-query log and the remainingCount it needed.
- Add DirectoryOutput::mirror, which writes the data of a native read as it is. - Pass on the partial first record ntdll returns with STATUS_BUFFER_OVERFLOW instead of ending the regular entries. - Check against ntdll that a listing goes on past a partial record.
- Write as much of a record as fits an empty output, with its full FileNameLength. - Report the partial record of a virtual entry with STATUS_BUFFER_OVERFLOW.
- Return the partial record of a virtual entry again until a buffer holds it. - Record the name of a virtual entry only once it is written whole.
- List a virtual entry whose file has a name too long for the buffer. - Cut a virtual entry short when neither name fits the buffer.
- Match the names of virtual entries as the filesystem matches its own, DOS wildcards and case beyond ASCII included. - List every virtual entry for an empty pattern, as ntdll lists every file for one. - Stop ignoring a leading dot of a name or a trailing ".*" of a pattern.
- Keep the source directories of a scan in a deque, which grows without copying them as a vector of them did.
…twice - Take the file name of a mapped path as UTF-16 directly, rather than convert it to UTF-8 and back.
- Latch the pattern per scan and keep it on a restart whose FileName is null or empty, as NTFS and ReFS do. - Gather the virtual entries with the latched pattern instead of the pattern of the restarting query. - Read the regular entries with the latched pattern, so that a reopened source directory keeps it too. - Add a differential test against a namesake directory, for queried and reopened handles.
- Mask the query flags with SL_QUERY_DIRECTORY_MASK as the I/O manager does, so SL_INDEX_SPECIFIED and undefined bits are ignored. - Leave queries with SL_RETURN_ON_DISK_ENTRIES_ONLY or SL_NO_CURSOR_UPDATE to ntdll, which lists only the physical directory. - Update SL_QUERY_DIRECTORY_MASK to the WDK value 0x1b and declare SL_NO_CURSOR_UPDATE. - Add a differential test of the query flags against ntdll and a namesake directory.
- Pass the NtOpenFile and NtClose calls of a directory scan straight to ntdll, so its handles are neither rerouted nor tracked and closing them takes no context lock. - Open scan directories with NtOpenFile for listing only, instead of CreateFileW. - Reopen the stored search path through the same function, without the hooks.
- Read the regular entries from the physical directory at the virtual path whenever the redirection tree maps entries into it, however the caller opened it. - Read the caller's handle when it lacks FILE_LIST_DIRECTORY, so the query fails as ntdll fails it. - Stop reading SearchHandleMap in the directory scan. - Add a differential test for a directory a mapping source is linked onto.
- Stop storing the search path of directory handles in the NtOpenFile and NtCreateFile hooks, as the directory scan no longer reads it. - Stop storing the find handle of FindFirstFileExW, which no query ever looked up and FindClose never removed. - Stop erasing the map in NtClose, which no longer takes the hook context lock. - Remove SearchHandleMap, SearchHandles and sharedids.h.
- Fold shadowed and deduplicated names one UTF-16 code unit at a time with RtlUpcaseUnicodeChar, as NTFS and ReFS do. - Stop merging names that differ in the case of a character beyond the BMP, which LCMapStringW folded. - Remove ush::to_upper, which has no callers left. - Add a test that checks which names are one against ntdll.
- Add UnmappedListing, which lists a directory outside the redirection tree through the hooks while mods are mapped elsewhere. - Share the mod mapping and the hook functions between the hooked benchmarks.
- Make a scan pass its queries through when the redirection tree does not hold the directory. - Skip opening a regular source and building the shadow set for such directories. - Stop logging scans of such directories, as master did not log them either.
- Copy the listable children of a directory out of the redirection tree as UTF-8 while holding the hook context. - Convert and match the names after releasing it, which halves how long a scan holds the context. - Take the hook context for reading, so that a scan takes the shared side once read and write access are split.
- Skip tree children without a link target when gathering virtual entries, as they map nothing. - List what of them exists on disk with the regular entries, in the same query as ntdll does. - Add a test that lists such children against ntdll.
- Drop the directory scan and the tracked path of a closed handle without asking for its file type, which took two system calls on every close. - Look a closed handle up under the shared lock of the handle tracker before taking it exclusively. - Drop the scan of a handle that is not on disk, such as the root of the named pipes, when it is closed.
- Add filler virtual names to the name folding test, so that the name sets meet case variants with more than a few buckets. - List the pair that differs in the case of a Latin-1 letter on its own, so that no name of that listing lies beyond U+00FF.
- Fold names while hashing and comparing them, so that looking a name up allocates nothing. - Fold ASCII inline and leave the other code units to RtlUpcaseUnicodeChar. - Read the names of records in usvfs's own buffers in place rather than copying them. - Gather the shadowed names only once a regular entry other than "." and ".." comes.
- Build the set that deduplicates virtual entries only when a virtual name is empty or not all ASCII. - Rely on the redirection tree, which folds the ASCII letters as RtlUpcaseUnicodeChar does, to keep names that are all ASCII apart.
- Note what reading ahead in larger reads would save and why the regular reader reads no more than the caller's buffer.
- Stop listing entries of the first mod's directory that the tree leaves out, such as skipped .mohidden files. - Read only "." and ".." from the rerouted caller handle when nothing lies at the virtual path on disk. - Treat a missing path, a file at the path and a directory pending deletion alike. - Recognize ".." when the smallest FileBothDirectoryInformation buffer cuts it short. - Return the open status from openScanDirectory and add DirectoryRecord::fileNameLength.
- Open directories for the scan sharing delete access, as FindFirstFileExW does. - Let others rename or delete a directory while a scan holds it open. - Read the physical directory when another handle holds it with DELETE access. - Test renaming a listed directory and listing one held for deletion against ntdll. - Fail the open by denying listing in the ACL, as a DELETE holder no longer does.
- Log the status of the failed open instead of GetLastError, which NtOpenFile does not set.
- Move the loop that writes the listed records of a read out of RegularEntryReader::fill.
- Spell out the presence check of the last record offset, as offset 0 is a valid value.
Illustar0
marked this pull request as ready for review
October 17, 2026 11:05
- Zero the 8.3 fields of every virtual entry, as NTFS and ReFS do for a record without a short name. - Drop the stock GetShortPathNameW call, which resolved the virtual name against the current directory. - Write virtual entries through appendRenamed even when the tree gives no virtual name. - Add tests for renamed records of the five Both classes and for a mapped C:\Program Files.
- Remove NtQueryDirectoryFileRegularFile, which ListsRegularEntriesLikeNtdll covers byte by byte. - Remove NtQueryDirectoryFileFindsVirtualFile, which ListsAVirtualEntryLikeNtdllListsItsFile covers for every class. - Remove NtQueryDirectoryFileExVirtualFile, as ExHandlesQueryFlagsLikeNtdll and RejectsMisalignedBuffersLikeNtdll list virtual entries through the Ex entry point.
- Drop NtQueryDirectoryFileListsSeveralVirtualEntriesPerQueryLikeNtdll, whose single-source listings ListsEntriesOfSeveralSourcesLikeNtdll repeats. - List the several sources for a single entry too, which only the dropped test did.
- Count hook contexts in HookContext::generation, which the hooks read without the lock. - Rebuild a scan on its next query once the context that built it is replaced, as the stock code dropped the scans it kept in the context. - Take over the pattern of the outdated scan and leave the native cursor of the caller's handle where it is. - Add a test that lists the mappings of a context replacing another.
- Forward the queries of a directory outside the redirection tree before asking for the mode of its handle, which saves the scan of an unmapped directory a NtQueryInformationFile call.
- Log as debug why a query on a mapped directory goes straight to ntdll. - Log as info a mapped directory that an asynchronous handle lists without its virtual entries. - Log as warn a directory on disk that a scan cannot open and lists through the caller's handle instead. - Log as info a scan rebuilt for a new hook context, and as debug a failed read of the entries on disk. - Add the handle, information class and query flags to the line each scan logs. - Name the mapped file or source directory in the warnings about virtual entries that cannot be read.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Closes #95.
While the diff stat shows
+5,694 / -1,128across 65 commits, over 3,600 of the added lines are unit tests, differential tests against nativentdll, and benchmarks (ntdll.cppitself shrinks by nearly 600 lines, and the core implementation is cleanly isolated indirectory_records.{h,cpp}anddirectory_scan.{h,cpp}).Every commit is atomic, builds cleanly, and passes the test suite on its own. That said, if the MO2 team would prefer to review this as a series of smaller staged PRs, I'd be happy to split it right away!
Overview
Previously, when enumerating virtual entries in a mapped directory,
hook_NtQueryDirectoryFile(Ex)operated one virtual file at a time: for every virtual match, it opened the source mod directory viaCreateFileW(re-entering USVFS's own open and attribute hooks under theHookContextlock), calledNtQueryDirectoryFiletwice, closed the handle, and returned at most one virtual record per caller buffer. During game startup with hundreds of thousands of virtual candidates, this caused a massive storm of redundant syscalls, hook re-entrancy, and lock acquisitions.ntdll.cpphasn't seen any fundamental architectural rework since roughly 2016, making it increasingly painful to keep piling workarounds on top of it—and the existing design itself had become a ceiling on further optimization. So I had to first "make the change easy" before making the performance improvements.In this PR, I reimplemented the
NtQueryDirectoryFile(Ex)hooks around a per-handleDirectoryScanand achieved major performance gains:DirectoryQuery.RegularEntryReadersynchronously reads the physical directory on disk and filters out entries shadowed by virtual ones.VirtualEntryReaderreads each mapping source directory at most once per scan.DirectoryOutputserves as the sole writer to the caller's buffer, handling record alignment,NextEntryOffsetlinking, and truncation in one place.Along the way, this PR fixes a wide range of long-standing edge cases in the hooks to match native
ntdllsemantics, backed by differential tests againstntdll.Performance
Test environment: JueLun v6.1 modlist on an i5-12600K with 529 active plugins, CRDW in
ACCELERATEmode (to avoid Skyrim's redundant startup directory traversals), and the latest versions of OAR, KID, and SPID. Measured via interleaved, paired A/B startup benchmarks with a warm filesystem cache after initial warm-up.Detailed Tables
hook_NtQueryDirectoryFileExinclusive time1DataLoaderthread startDataLoaderthread lifetime (plugin compilation &DataLoaded)DataLoaderblocked inWaitForDirectoryCache(waiting on OAR)Load Time Profilermain-menu report exportNtQueryDirectoryFile/FileExcalls2hook_NtQueryDirectoryFileExcallsNtCreateFile/NtOpenFilecalls3NtClosecalls4GetFileAttributesWcallsNtQueryAttributesFilecallsHookContext::readAccesslock acquisitionsVirtualEntryReader)NtQueryDirectoryFile/FileExNtCreateFile/NtOpenFile(incl.openScanDirectory)NtClose(via hook path)GetFileAttributesWNtQueryAttributesFileHookContextlock hold timeusvfs_x64.dllexclusive leaf CPU time6Main Breaking Changes
(a6a514e) Callers querying asynchronous handles, or passing an
Event,ApcRoutine, orApcContext(completion packet), will no longer see virtual entries. The legacy implementation only partially handled these and even forwarded the caller's Event and APC directly to internal reads backed by a stack-allocatedIO_STATUS_BLOCK, which is undefined behavior. (Full emulation of asynchronous notifications is feasible if ever needed, though it would require additionally hookingNtSetInformationFile.) Frankly, I'm not sure whether anything in the ecosystem relies on asynchronous directory queries, but it should be an exceedingly rare pattern—only applications calling the low-level NT API directly with an asynchronous handle, Event, or APC are affected. Personally, I'd much rather make a clean breaking change than keep a broken implementation for the illusion of compatibility.(6656902) Chained "virtual-to-virtual" mappings are no longer listed. Because the file-open hooks only perform a single-hop reroute, those files could never be opened in the first place, and re-entering the hooks to resolve them hurt performance.
Expected Behavior Changes
Below are the intended externally visible behavior changes beyond the main breaking changes above.
There may still be unintended behavior changes I introduced inadvertently, though the existing test suite and the new tests added here should minimize both their likelihood and their impact.
Expected Behavior Changes
Native Forwarding Rules
The following queries are now forwarded straight to
ntdll; forwarded queries see only the physical directory on disk:FileInformationClassvalues: returnntdll's status code instead of bogusly returningSTATUS_NO_SUCH_FILE(fa76500).ntdlldoes (67c38e1).SL_RETURN_ON_DISK_ENTRIES_ONLYandSL_NO_CURSOR_UPDATE(c874455):FindFirstFileEx(FIND_FIRST_EX_ON_DISK_ENTRIES_ONLY)now honors the flag's contract and enumerates only the physical directory.KernelBasekeeps passing this flag ((flags & 4) * 2) on every subsequentFindNextFilecall on that handle, so Win32 enumeration never mixes modes mid-scan.SL_INDEX_SPECIFIEDand undefined flag bits are ignored.Buffer & Status Output
IO_STATUS_BLOCK) (ccedf5f):Informationis set to 0 when enumeration ends.NextEntryOffsetthe same wayntdlldoes.Informationends at the exact end of the last record, without trailing alignment padding (beb05c5).STATUS_BUFFER_OVERFLOW(59f1d6e).STATUS_BUFFER_OVERFLOWand is repeated on subsequent calls until a buffer large enough to hold it is provided (e0bb294, 4bbf3d3).STATUS_NO_SUCH_FILEis returned only when the very first query on a handle yields no results. An empty result after a restart ends withSTATUS_NO_MORE_FILES(d34800b).Directory Enumeration & Matching
SL_RESTART_SCANalso restarts the physical portion of the scan (c7f09a8).FileNamekeeps the pattern latched by the first query, matching NTFS and ReFS (fdd30ff).NtOpenFile/NtCreateFilereissues that handle value (50bd6a6).RtlIsNameInExpression(cdd1a4a):foono longer falsely matches.foo, and a trailing.*is no longer treated as*.FILE_OPEN_FOR_BACKUP_INTENT. Caller handles lackingFILE_LIST_DIRECTORYaccess fail just as in nativentdll.RtlUpcaseUnicodeChar(dfe9dfa), no longer merging case variants outside the Basic Multilingual Plane (BMP).linkTargetare left to the on-disk listing and returned in the same query batch asntdll(96a46f5).meshes) list only.and..from the rerouted caller handle, followed by the virtual entries (87d91b6)..mohiddenfiles, skipped.gitdirectories, and files added to the mod after mapping. Those files were listed even though they could not be opened.FILE_SHARE_DELETE), matchingFindFirstFileExW(19887de):DELETEaccess can still be enumerated from disk.Changes Beyond Enumeration
SearchHandleMap(7ad82c0).FindFirstFileExW,NtOpenFile, andNtCreateFileno longer write state toHookContext.NtCloseno longer acquires theHookContextlock or callsGetFileType(e4fa037).NTSTATUSwhen opening a mapping source directory fails, rather than an unrelatedGetLastError(c4f6473).QueryFlagson both entry points (6fdfc61).testingfeature invcpkg.jsonfor a benchmark target not run by default underctest(53695da).Known Limitations & Trade-offs
{STATUS_NO_SUCH_FILE, 0}to the status block, whereas NTFS leaves it untouched; the hook follows NTFS.ntdll. The stock implementation wrote the status block unconditionally, matching ReFS but diverging from NTFS.NTSTATUSon failed queries..and..) only" depends on why opening the physical directory at the virtual path failed.NtCreateFiledoes not reroute when the path is a directory both on disk and in the virtual tree) and that directory is renamed or marked for deletion before the first query, the hook lists only.,.., and virtual entries, whereasntdllwould still list the entire physical directory.FILE_SHARE_DELETEin 19887de makes such renames easier to trigger. This case is not currently covered by tests.Eventon one call and omitting it on the next, or toggling flags mid-scan), the first virtualized call will start a fresh scan, which may return physical directory entries a second time.FindFirstFileEx/FindNextFile.hook_NtOpenFile, it points to the first mapped mod directory—matching what the stock implementation listed.a.txthides botha.txtandA.txtfrom the physical directory, consistent with how the redirection tree resolves names.GetShortPathNameWcould build paths that did not open or that led to another file.STATUS_NO_MORE_FILES, as in the stock implementation.Verification
Both unit and differential tests for this PR live in
tvfs_test, so you can also runtvfs_testdirectly.The differential tests run against
C:\Windowsand generated directories on the volume hosting the test binary. To cover ReFS, clone and run the repository on a ReFS volume.Benchmarks (Optional)
Compares
NativeListing/VirtualListing/UnmappedListingacrossmods ∈ {1, 8}andfiles ∈ {64, 512}. Use--benchmark_filter=to filter specific cases.Notes for Reviewers
Suggested reading order:
test/tvfs_test/directory_enumeration.cpp. The core production code lives indirectory_records.{h,cpp}(record layout parsing andDirectoryOutputbuffer writing) anddirectory_scan.{h,cpp}(RegularEntryReader,VirtualEntryReader, andDirectoryScanstate management)—you may find it easiest to read the final state of those two pairs of files directly.Reviewing commit-by-commit: every commit builds cleanly and passes the test suite on its own, structured around "First make the change easy, then make the easy change":
ntdll.cppand establish theDirectoryScan/DirectoryOutputskeleton, while fixing UB, resource leaks, dropped partial records, and hook re-entrancy when opening source directories that stood in the way of batching.ntdllsemantics orthogonal to batching (pattern matching and latching,SL_*flags, physical directory source, dot entries for purely virtual directories,FILE_SHARE_DELETE, etc.), along with follow-up performance work around theHookContextlock,SearchHandleMap,NtClose, and the name hash set.Commits labeled
refactorthat also change externally visible details:SearchHandleMapalong with the associated lock acquisition and logging inNtClose.refactorcommits are purely structural and should not change external behavior.Footnotes
Inclusive wall-clock time on the main thread inside
usvfs::hook_NtQueryDirectoryFileExprior toDataLoaderthread creation. Because inner hook/native calls andHookContextlock hold times nest and overlap, rows cannot be summed directly. ↩Counts non-overlapping low-level
Original_NtQueryDirectoryFile/FileExcall sites (includingforwardToNative), avoiding double-counting across outeraddNtSearchDataspans. ↩Combines native
ntdll_mess_NtCreateFile/NtOpenFilecalls with the 26,294 internalopenScanDirectoryNtOpenFilecalls added by the batched reader (which bypass outer hooks viaInternalIoScope). ↩Reflects only calls going through
usvfs::hook_NtClose; scan directory handles opened internally on the batched path are closed directly underInternalIoScope. ↩Native call times and lock hold durations nest and overlap, so rows cannot be summed directly. ↩
Exact ETW
SampledProfileCPU sample weights from two lossless paired runs (n=2per group). "USVFS call stacks" includes kernel (ntoskrnl.exe,Ntfs.sys,FltMgr.sys) and filter driver work (e.g., AV minifiltersysdiag.sys) triggered by USVFS hooks; "exclusive leaf CPU" counts only instructions executed directly insideusvfs_x64.dll. ↩ ↩2