Windows: the game starts from a folder named in any language (UTF-8 as its code page) #1191
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1191
Loading…
Reference in a new issue
No description provided.
Delete branch "windows-utf8-paths"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
On Windows the game did not start at all from a folder whose name has characters outside the system's ANSI code page: Japanese (or Cyrillic, Greek, Chinese...) on a Western-language Windows, or an emoji on any Windows. No window, and no hookshot_log.txt either.
Cause: host/ and gpu/vk keep paths as
charstrings and call the A functions with them (GetModuleFileNameA, CreateFileA, GetFileAttributesA, MoveFileExA, CreateDirectoryA, SetCurrentDirectoryA, fopen/freopen, about 180 call sites). Without a manifest those use the system's ANSI code page (1252 on a Western Windows), so a name that code page cannot spell comes back from GetModuleFileNameA with?in it. The game then cannot find image.bin, cannot open its log beside itself, and quits with exit code 2 and nothing on screen. The code already treats itscharstrings as UTF-8 everywhere it converts them (hos_update.c, thread names, every MultiByteToWideChar/WideCharToMultiByte is CP_UTF8), so the A calls were the one part that didn't match.A Windows tester confirmed exactly this: from a folder named
Hookshot-DLSS ゼルダon English Windows, the game read its own path asHookshot-DLSS ???\hookshot.exe, printed its usage line and exited with code 2. In the same tester's hookshot_log.txt, thefs: mounted ...lines (host paths from GetModuleFileNameA) showedéas�(ANSI 1252 bytes), while the other lines were UTF-8.The fix: hookshot.exe now has an application manifest with
<activeCodePage>UTF-8</activeCodePage>(nx2/win_manifest.py writes it as a .res in pure Python, and nx2/build.py links it in beside the icon's .res, so no rc/windres/mt is needed with either llvm-mingw or the MSVC target). From Windows 10 1903 on, this makes the process's code page UTF-8: every A function, the UCRT's fopen/_open/stat/getenv and argv take and give UTF-8, in hookshot.exe and in every DLL it loads (hookshot_vk.dll, SDL, FFmpeg). One linked resource covers all ~180 call sites, the C runtime and the third-party code, and new code written the same way keeps working. The manifest also saysasInvoker(no change: a 64-bit process gets no UAC virtualization anyway) and nothing else, so DPI awareness, comctl version and OS compatibility stay as they were.Why not convert every call to the W functions: that is ~180 call sites in host/ and gpu/vk, plus every fopen, stat, opendir and getenv on a path, plus wrappers for CRT functions that have no W twin, and any new narrow call would bring the bug back. The manifest is the approach Microsoft recommends for code that keeps UTF-8
charstrings, and the codebase already did. What it does not cover is Windows older than 10 1903 (ignored there). The readme already asks for Windows 10 or 11, and 1903 (May 2019) and all older Windows 10 releases are out of support, so for those this PR adds a clear message instead of a silent exit.Start-up check (host/hos_main.c, narrow_paths_ok): before anything opens a file, the game checks that GetModuleFileNameA's path names the exe. If it doesn't (an older Windows with such a folder name, or a path longer than MAX_PATH bytes), it writes a line to stderr and shows a message box naming the folder and what to do (move it to a plainer, shorter path, or update Windows), then exits. The message box is skipped for
--prepare-shaders(the builder's last step, nothing to click) and with HOS_HEADLESS. On a working setup the check costs one GetModuleFileNameA and one GetFileAttributesA.Command line and logs: under the manifest, GetCommandLineA, and so the C runtime's
argv, are UTF-8 too, somain(argc, argv)gets the same textwmainwould, without changing how arguments are parsed (--romfs/--userpaths and image.bin's path included). hookshot_log.txt is opened with freopen on the UTF-8 path, and every line is UTF-8 now: thefs:lines' host paths included, which were ANSI bytes before (éshowed as�), or?.Audit notes: the launcher and setup (builder/setup/setup.c) already use only W functions and -municode, so they needed nothing. The Hookshot Builder (.NET) and the build (Python, clang, lld) are Unicode already (and #200 covers the builder children's output). The Vulkan loader is loaded by name. Nothing in host/ or gpu/vk converts with CP_ACP, CP_OEMCP or CP_THREAD_ACP, and nothing calls SetFileApisToOEM. hos_update.c and DLSS's own path code use W functions, which this doesn't change. Linux doesn't change: the manifest is only linked into hookshot.exe, and the check is
_WIN32only.Known limit: a path is limited to MAX_PATH bytes in UTF-8, so about 85 characters of Japanese (3 bytes each) instead of about 130 with Japanese Windows's CP932 (2 bytes each). The start-up check reports such a path clearly instead of failing silently.
Tests: tests/test_utf8_paths.py (in run_all.py) checks that the .res holds one RT_MANIFEST, resource 1, with activeCodePage UTF-8 and asInvoker; that nx2/build.py links it into hookshot.exe; that main runs narrow_paths_ok before anything else; and that host/ and gpu/vk (not host/posix or the vendored code) convert no text with an ANSI code page. It also builds tests/utf8_paths_test.c (with the real narrow_paths_ok pasted in) for Windows, once with the manifest and once without. On Windows (CI's windows-2022 job, when clang is on PATH) it runs both from a folder named
ゲーム Zélda 🎮. With the manifest: GetACP() is 65001, the start-up check passes, a UTF-8/UTF-16 round trip ofC:\ゲーム\Zélda 🎮\(surrogate pair included) is exact, GetModuleFileNameA equals GetModuleFileNameW in UTF-8,argvhasHookshot-DLSS ゼルダ Zéldain UTF-8, a line printed into a freopen'd stderr (as hookshot_log.txt is) with the exe's path holds its UTF-8 bytes, a file made with fopen on the UTF-8 path is the file GetFileAttributesW/DeleteFileW find, and getenv gives UTF-8. Without it (the negative control, skipped on a PC set to "Use Unicode UTF-8 for worldwide language support"): the start-up check refuses the folder, as the game used to fail. On Linux, with HOOKSHOT_MINGW set, the probe is cross-built and llvm-readobj confirms the manifest resource is in it and not in the control.Verified: the Linux build, and
python -I -B tests/run_all.pyon Linux (140 passed, 0 failed, 7 skipped; with HOOKSHOT_MINGW set, the probe is cross-built and its manifest checked). A full Windows cross-build of the game with llvm-mingw (UCRT): hookshot.exe and hookshot_vk.dll link,llvm-readobj --coff-resources hookshot.exeshows the RT_MANIFEST (resource 1) with activeCodePage UTF-8, and the start-up message is in the exe. I haven't run it on Windows itself (no Windows or Wine here), so there is a checklist for a tester below.For a Windows tester (please check on Windows 10 1903+ or 11, on a Western-language Windows without the "Beta: Use Unicode UTF-8" option turned on):
C:\Games\ゼルダ Zélda 🎮and start hookshot.exe from it: the game starts and plays as usual.Hookshot-DLSS ゼルダon English Windows. Before, the game read its path asHookshot-DLSS ???\hookshot.exe, printed its usage line and exited with code 2. Now it starts.fs: mounted ... -> <path>lines showゼルダ,éand emoji correctly, with no?or�.hookshot.exe --prepare-shadersthere).