@@ -57,9 +57,9 @@ static void dump_init(void){
5757 * ~/Library/Logs/NFSU2/). For scenes the user reaches but automated tests
5858 * can't produce (e.g. the end of a career race). */
5959static _Atomic int g_dump_req;
60- static long g_dump_req_f = -1;
60+ static long g_dump_req_f = -1, g_dump_req_end = -1; /* Cmd+Shift+D: frames [f, end] */
6161static int dump_wanted(long f){
62- if (f == g_dump_req_f) return 1;
62+ if (g_dump_req_f >= 0 && f >= g_dump_req_f && f <= g_dump_req_end ) return 1;
6363 for (int i = 0; i < g_dump_n; i++) if (g_dump_want[i] == (int)f) return 1;
6464 return 0;
6565}
@@ -4491,35 +4491,58 @@ void metal_set_ps(int psid, const float *c32){
44914491 * The game's own bytecode is translated to MSL and compiled here. */
44924492#define MAXVSF 512
44934493static id<MTLFunction> g_vsfn[MAXVSF];
4494+ /* The compile runs WITHOUT the big lock (guest_blocking_begin): a new shader is
4495+ * compiled synchronously on the game thread when it is first drawn, and with the
4496+ * HD capture kernel that took 50-110 ms when a race started (2026-11-04 session:
4497+ * the sound threads waited 50-64 ms for the lock -> the sound stuttered between
4498+ * the loading screen and the race). Nothing here touches guest memory; the
4499+ * results are stored after the lock is back. VSCOMPILELOCK=1: keep the lock. */
44944500void metal_register_vs(int vsid, const char *msl){
44954501 if (vsid <= 0 || vsid >= MAXVSF || !msl || !g_dev || g_vsfn[vsid]) return;
44964502 @autoreleasepool {
44974503 NSString *src = [kPrelude stringByAppendingString:
44984504 [NSString stringWithUTF8String:msl]];
44994505 NSError *err = nil;
4506+ int guest_blocking_begin(void); void guest_blocking_end(void);
4507+ static int keep = -1; if (keep < 0) keep = ENVC("VSCOMPILELOCK") != NULL;
4508+ int yielded = keep ? 0 : guest_blocking_begin();
4509+ id<MTLFunction> vfn = nil;
4510+ #if NFS_ENHANCED
4511+ id<MTLComputePipelineState> vsk = nil; NSError *ke = nil; int want_k = 0;
4512+ #endif
45004513 struct timespec _c0; if (g_perflog) clock_gettime(CLOCK_MONOTONIC, &_c0);
45014514 id<MTLLibrary> lib = [g_dev newLibraryWithSource:src options:nil error:&err];
45024515 if (g_perflog){ struct timespec _c1; clock_gettime(CLOCK_MONOTONIC, &_c1);
45034516 perflog_event(PL_VS, (uint64_t)(_c1.tv_sec - _c0.tv_sec) * 1000000000ull + (uint64_t)_c1.tv_nsec - (uint64_t)_c0.tv_nsec, 0); }
4517+ char nm[32];
4518+ if (lib){
4519+ snprintf(nm, sizeof nm, "vs_%d", vsid);
4520+ vfn = [lib newFunctionWithName:[NSString stringWithUTF8String:nm]];
4521+ #if NFS_ENHANCED
4522+ if (vsid < HD_MAXVSI && hd_rt_on()){
4523+ want_k = 1;
4524+ snprintf(nm, sizeof nm, "vsk_%d", vsid);
4525+ id<MTLFunction> kf = [lib newFunctionWithName:[NSString stringWithUTF8String:nm]];
4526+ if (kf) vsk = [g_dev newComputePipelineStateWithFunction:kf error:&ke];
4527+ }
4528+ #endif
4529+ }
4530+ if (yielded) guest_blocking_end();
45044531 if (!lib){
45054532 static int shown = 0;
45064533 if (shown < 5){ shown++;
45074534 fprintf(stderr, "vertex shader %d could not be compiled: %s\n", vsid,
45084535 err ? [[err localizedDescription] UTF8String] : "?"); }
45094536 return;
45104537 }
4511- char nm[32]; snprintf(nm, sizeof nm, "vs_%d", vsid);
4512- g_vsfn[vsid] = [lib newFunctionWithName:[NSString stringWithUTF8String:nm]];
4513- if (!g_vsfn[vsid])
4538+ g_vsfn[vsid] = vfn;
4539+ if (!vfn)
45144540 fprintf(stderr, "vertex shader %d: function not found\n", vsid);
45154541#if NFS_ENHANCED
4516- if (vsid < HD_MAXVSI && hd_rt_on()){
4517- snprintf(nm, sizeof nm, "vsk_%d", vsid);
4518- id<MTLFunction> kf = [lib newFunctionWithName:[NSString stringWithUTF8String:nm]];
4519- NSError *ke = nil;
4520- if (kf) g_hd_vsk[vsid] = [g_dev newComputePipelineStateWithFunction:kf error:&ke];
4521- if (!g_hd_vsk[vsid]) fprintf(stderr, "[HD] capture kernel vs_%d could not be built: %s\n", vsid,
4522- ke ? [[ke localizedDescription] UTF8String] : "no function");
4542+ if (want_k){
4543+ g_hd_vsk[vsid] = vsk;
4544+ if (!vsk) fprintf(stderr, "[HD] capture kernel vs_%d could not be built: %s\n", vsid,
4545+ ke ? [[ke localizedDescription] UTF8String] : "no function");
45234546 }
45244547#endif
45254548 }
@@ -6954,13 +6977,17 @@ + (unsigned long long)_d1.tv_nsec - (unsigned long long)_d0.tv_nsec);
69546977 { static long yk = -2; if (yk == -2){ const char *e = ENVC("YAKALAKARE"); yk = e ? atol(e) : -1; }
69556978 if (fno_pre == yk) atomic_store(&g_dump_req, 1); } /* test of the shortcut */
69566979 if (atomic_exchange(&g_dump_req, 0)){
6957- g_dump_req_f = fno_pre;
6980+ /* a burst of consecutive frames (CAPBURST, default 8): flicker
6981+ * only shows between frames; the draw list is written for the first */
6982+ static long burst = -1;
6983+ if (burst < 0){ const char *e = ENVC("CAPBURST"); burst = e ? atol(e) : 8; if (burst < 1) burst = 1; if (burst > 64) burst = 64; }
6984+ g_dump_req_f = fno_pre; g_dump_req_end = fno_pre + burst - 1;
69586985 if (!g_dump_n){ const char *pl = getenv("PERFLOG"), *h = getenv("HOME");
69596986 if (pl && *pl) snprintf(g_dump_dir, sizeof g_dump_dir, "%s", pl);
69606987 else snprintf(g_dump_dir, sizeof g_dump_dir, "%s/Library/Logs/NFSU2", h ? h : "/tmp"); }
6961- fprintf(stderr, "capture: frame %ld (Cmd+Shift+D) -> %s\n", fno_pre, g_dump_dir);
6988+ fprintf(stderr, "capture: frames %ld-%ld (Cmd+Shift+D) -> %s\n", fno_pre, g_dump_req_end , g_dump_dir);
69626989 }
6963- int dumpnow = (g_dump_n || g_dump_req_f == fno_pre) && dump_wanted(fno_pre);
6990+ int dumpnow = (g_dump_n || ( g_dump_req_f >= 0 && fno_pre >= g_dump_req_f && fno_pre <= g_dump_req_end) ) && dump_wanted(fno_pre);
69646991 /* GLYPHDUMP: screen box + UV of every quad.
69656992 * Goal: decode the TEXT the game writes and read its state. */
69666993 if (ENVC("GLYPHDUMP") && dumpnow && rb >= 0){
0 commit comments