Skip to content

Commit 243630d

Browse files
developerclaude
andcommitted
Expand %p in X87_PROFILE and X87_SAMPLE to the attached process pid
In cooperative mode Wine re-executes every 32-bit process through the sidecar, so one game launch is several sidecars: the wineboot helpers, the process that starts the game, and the game itself, all inheriting the same X87_PROFILE. The last sidecar to open the path wins, and since the block profiler appends its counter section only after its target exits, the game's profile was routinely truncated by a helper's before it could be finalised. profile_analyze then rejected it as incomplete, with no way to tell which process the surviving file belonged to. Expand %p against the pid behind the task port the sidecar received, for both X87_PROFILE and X87_SAMPLE, before any old output is unlinked or a new file opened. Without %p the paths behave as before. If the target pid cannot be resolved the profiler is disabled with a message rather than writing under the wrong name. Verified under wine-cx-26.3.0-1 on an M2 Max, macOS 26.5, with a 32-bit Direct3D game: one launch produced x87-block-<pid>.prof for each of four cooperative sidecars, and the game's file carried a complete counter section that profile_analyze accepted (2197 blocks). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 4e9c738 commit 243630d

2 files changed

Lines changed: 50 additions & 10 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,11 @@ achieved.
201201
| `X87_NO_UNWIND=1` | `--no-unwind` | off | leaf pcs only, about half the per-sample cost |
202202

203203
The environment wins over the flags, so an app bundle can enable sampling
204-
without touching argv.
204+
without touching argv. `%p` in the `X87_SAMPLE` or `X87_PROFILE` path expands
205+
to the pid of the process the sidecar is attached to, not the sidecar's own.
206+
Wine starts one cooperative sidecar per i386 process and all of them inherit
207+
the same environment, so a fixed path would let the injector's sidecar
208+
truncate the game's profile, or the reverse.
205209

206210
### Block profiler: what the x87 code costs
207211

‎rosetta_loader/src/sidecar.cpp‎

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,14 +2902,48 @@ bool installPortInParent(mach_port_t parentTaskPort, mach_port_t* outServicePort
29022902
return true;
29032903
}
29042904

2905+
// A launcher can start several independently wrapped processes with the same
2906+
// profiler environment. Expand against the process whose task port the
2907+
// sidecar received, not the sidecar's own pid, so they cannot truncate one
2908+
// another's output.
2909+
static bool expandTargetPid(std::string& path, pid_t targetPid, const char* variable) {
2910+
if (path.find("%p") == std::string::npos) {
2911+
return true;
2912+
}
2913+
if (targetPid <= 0) {
2914+
fprintf(stdout,
2915+
"[rosettax87] %s: cannot expand %%p because the target pid is unavailable; "
2916+
"profiling disabled\n",
2917+
variable);
2918+
return false;
2919+
}
2920+
2921+
const std::string replacement = std::to_string(targetPid);
2922+
size_t offset = 0;
2923+
while ((offset = path.find("%p", offset)) != std::string::npos) {
2924+
path.replace(offset, 2, replacement);
2925+
offset += replacement.size();
2926+
}
2927+
return true;
2928+
}
2929+
29052930
bool spawnReceiveThread(mach_port_t servicePort, mach_port_t parentTaskPort) {
29062931
if (g_rosetta_config != nullptr && !g_rosetta_config->profile_path.empty()) {
2907-
const char* path = g_rosetta_config->profile_path.c_str();
2908-
g_profile.file = std::fopen(path, "wb");
2932+
std::string path = g_rosetta_config->profile_path;
2933+
pid_t targetPid = 0;
2934+
pid_for_task(parentTaskPort, &targetPid);
2935+
if (!expandTargetPid(path, targetPid, "X87_PROFILE")) {
2936+
path.clear();
2937+
}
2938+
g_profile.file = path.empty() ? nullptr : std::fopen(path.c_str(), "wb");
29092939
if (g_profile.file == nullptr) {
2910-
fprintf(stdout, "[rosettax87] X87_PROFILE: failed to open '%s' for writing\n", path);
2940+
if (!path.empty()) {
2941+
fprintf(stdout, "[rosettax87] X87_PROFILE: failed to open '%s' for writing\n",
2942+
path.c_str());
2943+
}
29112944
} else {
2912-
fprintf(stdout, "[rosettax87] X87_PROFILE: dumping IR streams to '%s'\n", path);
2945+
fprintf(stdout, "[rosettax87] X87_PROFILE: dumping IR streams to '%s'\n",
2946+
path.c_str());
29132947
}
29142948
fflush(stdout);
29152949
}
@@ -3026,6 +3060,13 @@ void startSampler(mach_port_t parentTaskPort, uint64_t runtimeBase, const Sample
30263060
// Sweeping every thread faster than the sample rate is never what was
30273061
// meant: the sweep is the expensive mode and the rate is the cheap one.
30283062
cfg.sweep_interval_us = std::max(cfg.sweep_interval_us, cfg.interval_us);
3063+
// The target's pid is needed by both threads and pid_for_task only answers
3064+
// while it is alive, so read it before touching the output path.
3065+
pid_t targetPid = 0;
3066+
pid_for_task(parentTaskPort, &targetPid);
3067+
if (!expandTargetPid(cfg.path, targetPid, "X87_SAMPLE")) {
3068+
return;
3069+
}
30293070
// Nothing from a previous run may survive into this one: a run that never
30303071
// latches writes no profile, and the absence has to be the answer rather
30313072
// than the last run's file still sitting there to be read as this one's.
@@ -3037,11 +3078,6 @@ void startSampler(mach_port_t parentTaskPort, uint64_t runtimeBase, const Sample
30373078
unlink(cfg.path.c_str());
30383079
unlink((cfg.path + ".windows").c_str());
30393080
unlinkWindowSeries(cfg.path);
3040-
// The target's pid is needed by both threads and pid_for_task only answers
3041-
// while it is alive, so it is read here, once, before either starts.
3042-
pid_t targetPid = 0;
3043-
pid_for_task(parentTaskPort, &targetPid);
3044-
30453081
pthread_attr_t attr;
30463082
pthread_attr_init(&attr);
30473083
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

0 commit comments

Comments
 (0)