The prepare step: with a window, the game's first submission waits until the prewarm lists (pso_manifest.tsv, then pso_user.tsv) are built, on every core at user-initiated QoS, behind a "Preparing shaders" screen that shows only past 0.5 s and with 0.3 s or more still to go. nvn_mtl.m: the prewarm reads its lists into tasks (warm_parse): a shader, a compute pipeline, or all render pipelines of one shader pair, which share their bases, so no two workers compile the same base; shaders go into one dictionary the workers share, and a vertex shader's pulling entry is loaded with it (as shader_prefetch_rest does), so neither a draw nor its hybrid stand-in loads a library the lists name. warm_prepare runs the tasks on hw.activecpu workers (DM_PSO_PREPARE_THREADS=<n>) from mtl_sub_begin's first call, before anything of that submission is encoded, and gives the front end done of total every 50 ms once 0.5 s have passed, unless the rate so far leaves less than 0.3 s; the shaders and pipelines go into the caches the draws use at once (warm_claim), which keep them for the run. It runs at the first submission and not before the game starts because the vertex entry each pipeline is built with follows the depth mode the game sets (nvnDeviceSetDepthMode). It runs at every start: with the Metal cache and the pipeline archive warm it takes well under a second and shows nothing, and a stamp that skipped it would skip it exactly when the system has dropped the Metal cache (an OS update, storage pressure on iOS). Without a window, or with DM_PSO_PREPARE=0, the prewarm runs in the background on one worker as before; DM_PSO_PREPARE=1 makes a windowless run wait too. The log line: pipeline prepare: N lines (shaders, render and compute pipelines) in s on N workers before the first frame, progress screen shown or not; bases compiled, from the archive, reused. front.h front_progress: front_mac.m draws the screen with AppKit views over the game's view, front_ios.m with UIKit views over the game view and the pad (a dark view, the title, a bar and a count); nvn_mtl.m keeps a weak empty one for the unit tests. NVN only: the Vulkan and GL layers keep no prewarm lists
This commit is contained in:
1 parent
9607f51022
commit
7a47d1c5eb
5 files changed
+210
-67
No files matched your search
+148
-66
@@ -69,6 +69,9 @@ static uint64_t c2_handle(int stage, unsigned b); // the handle c2 holds at by
|
||||
#define MDIE(...) do { MLOG("FATAL " __VA_ARGS__); abort(); } while (0)
|
||||
|
||||
static void warm_start(void);
|
||||
static void shmap_load(void);
|
||||
static int prepare_on(void);
|
||||
static int g_warm_started;
|
||||
static void warm_claim(void);
|
||||
|
||||
static id<MTLDevice> g_dev;
|
||||
@@ -694,6 +697,7 @@ void mtl_sub_begin(void) {
|
||||
mtl_init(); g_sub_serial++; g_seg = 0; seg_clk_put(g_sclk); g_sclk = NULL; g_rc_npend = 0; g_rc_skip = 0;
|
||||
defer_drain();
|
||||
defer_reset();
|
||||
if (!g_warm_started && prepare_on()) shmap_load(); // the first submission: the prepare step (warm_start), before anything of it is encoded
|
||||
warm_claim();
|
||||
if (g_cb) MDIE("submit re-entered");
|
||||
g_sub_thread = pthread_self(); atomic_store(&g_sub_thread_set, 1);
|
||||
@@ -3069,10 +3073,9 @@ static void c1_store(NvShader *o) {
|
||||
static NSMutableArray<WarmItem *> *g_warm_items;
|
||||
static os_unfair_lock g_warm_lock = OS_UNFAIR_LOCK_INIT;
|
||||
static _Atomic int g_warm_ready;
|
||||
static int g_warm_started;
|
||||
static uint64_t g_warm_t0;
|
||||
static _Atomic int g_warm_running; // Background warm-up has not finished yet (the loading screen on entering a run waits a little longer based on this, see mtl_warm_wait)
|
||||
static uint64_t g_warm_shaders, g_warm_rps, g_warm_cps, g_warm_skip_fz, g_warm_skip_stale;
|
||||
static _Atomic uint64_t g_warm_shaders, g_warm_rps, g_warm_cps, g_warm_skip_fz, g_warm_skip_stale; // the prewarm's workers count into them
|
||||
static uint64_t g_warm_claim_shaders, g_warm_claim_rps, g_warm_claim_cps;
|
||||
static id<MTLFunction> bf_fn(NvShader *fs, const int *fx, uint8_t fbmask, const uint8_t *lmap);
|
||||
static int rt_range_bindings_ro(NSDictionary *j);
|
||||
@@ -5047,18 +5050,30 @@ static NSString *warm_key(const char *s, size_t n) {
|
||||
size_t z = strnlen(s, n);
|
||||
return [[NSString alloc] initWithBytes:s length:z encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
// The prewarm runs the lines of its lists as tasks: a shader (S), a compute pipeline (C), or every render pipeline (R) of one shader pair. The pipelines of a pair share their
|
||||
// bases, so one worker builds them one after the other and no two workers compile the same base. The tasks keep the lists' order (a pair at its first line).
|
||||
// Shaders go into one dictionary all workers share (a key two workers load at once is kept once; a shader that failed to load is kept as NSNull and not tried again)
|
||||
@interface WarmTask : NSObject { @public int kind, stage; NSString *key, *vkey, *fkey; NSMutableArray<NSData *> *rs; } @end
|
||||
@implementation WarmTask @end
|
||||
static NSMutableDictionary *g_warm_dict; static os_unfair_lock g_warm_dict_lock = OS_UNFAIR_LOCK_INIT;
|
||||
static _Atomic long g_warm_done; // lines done (the prepare screen's count)
|
||||
// A prewarm list may name shaders of another version of the game: the bundled list of the version the profile was brought up on, packed with another version (the NSP script's
|
||||
// derived profile: Hollow Knight: Silksong's 1.0.30000 list named 16 shaders its base game 1.0.27776 does not have), or a device's pso_user.tsv kept across a game update.
|
||||
// A key the shader map does not have is skipped quietly (counted in the "prewarm done" line); the game never draws with it
|
||||
static NvShader *warm_shader(NSMutableDictionary<NSString *, NvShader *> *dict, NSString *key, int stage) {
|
||||
NvShader *sh = dict[key];
|
||||
// A line with a key the shader map does not have is dropped quietly (the keys are counted in the "prewarm done" line); the game never draws with it
|
||||
static int warm_stale(NSString *key, NSMutableSet<NSString *> *stale) {
|
||||
NSString *mk = [key hasSuffix:@"_b"] ? [key substringToIndex:key.length - 2] : key; // library names are <map key>_b
|
||||
if (g_shmap[mk]) return 0;
|
||||
if (![stale containsObject:key]) { [stale addObject:key]; g_warm_skip_stale++; }
|
||||
return 1;
|
||||
}
|
||||
static NvShader *warm_shader(NSString *key, int stage) {
|
||||
os_unfair_lock_lock(&g_warm_dict_lock); id sh = g_warm_dict[key]; os_unfair_lock_unlock(&g_warm_dict_lock);
|
||||
if (!sh) {
|
||||
NSString *mk = [key hasSuffix:@"_b"] ? [key substringToIndex:key.length - 2] : key; // library names are <map key>_b
|
||||
static NSMutableSet<NSString *> *stale; if (!stale) stale = [NSMutableSet set]; // only the warm-up thread calls this
|
||||
if (!g_shmap[mk]) { if (![stale containsObject:key]) { [stale addObject:key]; g_warm_skip_stale++; } return nil; }
|
||||
sh = shader_load(key, stage, 0); if (sh) dict[key] = sh;
|
||||
NvShader *n = shader_load(key, stage, 0);
|
||||
if (n && n->stage == 0 && n->has_pull && !n->layer_out && hybrid_on()) pull_load(n); // its pulling entry too, as shader_prefetch_rest loads it, so a draw that falls back to it loads nothing (no other thread has n yet)
|
||||
os_unfair_lock_lock(&g_warm_dict_lock); sh = g_warm_dict[key]; if (!sh) g_warm_dict[key] = sh = n ?: (id)NSNull.null; os_unfair_lock_unlock(&g_warm_dict_lock);
|
||||
}
|
||||
return sh;
|
||||
return sh == NSNull.null ? nil : sh;
|
||||
}
|
||||
static void warm_enqueue(WarmItem *item) {
|
||||
os_unfair_lock_lock(&g_warm_lock);
|
||||
@@ -5066,62 +5081,79 @@ static void warm_enqueue(WarmItem *item) {
|
||||
atomic_store_explicit(&g_warm_ready, 1, memory_order_release);
|
||||
os_unfair_lock_unlock(&g_warm_lock);
|
||||
}
|
||||
static void warm_worker(NSArray<NSString *> *paths) {
|
||||
@autoreleasepool {
|
||||
NSMutableDictionary<NSString *, NvShader *> *dict = [NSMutableDictionary dictionary];
|
||||
NSMutableSet<NSString *> *seen = [NSMutableSet set];
|
||||
NSMutableSet<NSData *> *seen_k = [NSMutableSet set]; // rows recorded before pso_key_norm that are one pipeline now
|
||||
for (NSString *path in paths) {
|
||||
NSString *txt = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
|
||||
if (!txt) { MLOG("[RED] cannot read prewarm list: %s", path.UTF8String); continue; }
|
||||
{ unsigned long nl = 0; for (NSString *l in [txt componentsSeparatedByString:@"\n"]) nl += l.length && ![l hasPrefix:@"#"];
|
||||
MLOG("pipeline prewarm list %s: %lu lines", path.UTF8String, nl); } // Diagnostics: how many lines each manifest gave (when a device warms up 0, shows which one was missing)
|
||||
for (NSString *line in [txt componentsSeparatedByString:@"\n"]) {
|
||||
if (!line.length || [line hasPrefix:@"#"] || [seen containsObject:line]) continue;
|
||||
[seen addObject:line];
|
||||
NSArray<NSString *> *c = [line componentsSeparatedByString:@"\t"];
|
||||
if (!c.count) continue;
|
||||
if ([c[0] isEqualToString:@"S"] && c.count >= 3) {
|
||||
NSString *key = c[1]; NvShader *sh = warm_shader(dict, key, c[2].intValue);
|
||||
if (!sh) continue;
|
||||
WarmItem *item = [WarmItem new]; item->kind = 0; item->key = key; item->shader = sh;
|
||||
warm_enqueue(item); g_warm_shaders++;
|
||||
} else if ([c[0] isEqualToString:@"R"] && c.count >= 2) {
|
||||
pso_skey sk; if (pso_skey_parse(c[1].UTF8String, &sk)) continue;
|
||||
NSData *kd = [NSData dataWithBytes:&sk length:sizeof sk]; if ([seen_k containsObject:kd]) continue;
|
||||
[seen_k addObject:kd];
|
||||
NSString *vkey = warm_key(sk.vs, sizeof sk.vs), *fkey = warm_key(sk.fs, sizeof sk.fs);
|
||||
NvShader *vs = vkey.length ? warm_shader(dict, vkey, sk.vs_stage) : nil;
|
||||
NvShader *fs = fkey.length ? warm_shader(dict, fkey, sk.fs_stage) : nil;
|
||||
if (!vs || (fkey.length && !fs)) continue;
|
||||
id<MTLFunction> fsfn = nil;
|
||||
if (sk.fz) {
|
||||
if (sk.fz_desc.kind == 1 && fs) { int fx[32]; for (int i = 0; i < 32; i++) fx[i] = sk.fz_desc.fx[i]; fsfn = bf_fn(fs, fx, sk.fz_desc.fbmask, sk.k.lmap); }
|
||||
if (!fsfn) { g_warm_skip_fz++; continue; }
|
||||
}
|
||||
pso_key k = sk.k; k.vs = (__bridge void *)vs; k.fs = (__bridge void *)fs; k.fsfn = (__bridge void *)fsfn; // pso_skey_parse normalized rows written before the key was
|
||||
NSError *e = nil; id ps = pso_build(pso_desc(&k, vs, fs, fsfn, sk.merged), &e);
|
||||
if (!ps && !vs->layer_out && [e.localizedDescription containsString:@"render_target_array_index"]) { // a vertex shader that writes the layer (see pso_get_)
|
||||
vs->layer_out = 1; e = nil; ps = pso_build(pso_desc(&k, vs, fs, fsfn, sk.merged), &e); }
|
||||
if (!ps) { MLOG("[RED] background render pipeline creation failed: %s", e.localizedDescription.UTF8String); continue; }
|
||||
WarmItem *item = [WarmItem new]; item->kind = 1; item->vs_key = vkey; item->fs_key = fkey; item->vs = vs; item->fs = fs; item->fsfn = fsfn; item->pso = k; item->object = ps;
|
||||
warm_enqueue(item); g_warm_rps++;
|
||||
} else if ([c[0] isEqualToString:@"C"] && c.count >= 2) {
|
||||
NSString *key = c[1]; if (!key.length) continue;
|
||||
NvShader *cs = warm_shader(dict, key, 5);
|
||||
if (!cs) continue;
|
||||
NSError *e = nil; id ps = [g_dev newComputePipelineStateWithFunction:cs->fn error:&e];
|
||||
if (!ps) { MLOG("[RED] background compute pipeline creation failed: %s", e.localizedDescription.UTF8String); continue; }
|
||||
WarmItem *item = [WarmItem new]; item->kind = 2; item->key = key; item->shader = cs; item->object = ps;
|
||||
warm_enqueue(item); g_warm_cps++;
|
||||
}
|
||||
}
|
||||
// The lists' lines as tasks; *lines: the lines they hold
|
||||
static NSArray<WarmTask *> *warm_parse(NSArray<NSString *> *paths, long *lines) {
|
||||
NSMutableArray<WarmTask *> *tasks = [NSMutableArray array]; NSMutableDictionary<NSString *, WarmTask *> *pairs = [NSMutableDictionary dictionary];
|
||||
NSMutableSet<NSString *> *seen = [NSMutableSet set], *stale = [NSMutableSet set]; long n = 0;
|
||||
NSMutableSet<NSData *> *seen_k = [NSMutableSet set]; // rows recorded before pso_key_norm that are one pipeline now
|
||||
for (NSString *path in paths) {
|
||||
NSString *txt = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
|
||||
if (!txt) { MLOG("[RED] cannot read prewarm list: %s", path.UTF8String); continue; }
|
||||
{ unsigned long nl = 0; for (NSString *l in [txt componentsSeparatedByString:@"\n"]) nl += l.length && ![l hasPrefix:@"#"];
|
||||
MLOG("pipeline prewarm list %s: %lu lines", path.UTF8String, nl); } // Diagnostics: how many lines each manifest gave (when a device warms up 0, shows which one was missing)
|
||||
for (NSString *line in [txt componentsSeparatedByString:@"\n"]) {
|
||||
if (!line.length || [line hasPrefix:@"#"] || [seen containsObject:line]) continue;
|
||||
[seen addObject:line];
|
||||
NSArray<NSString *> *c = [line componentsSeparatedByString:@"\t"];
|
||||
WarmTask *t = nil;
|
||||
if (([c[0] isEqualToString:@"S"] && c.count >= 3) || ([c[0] isEqualToString:@"C"] && c.count >= 2)) {
|
||||
if (!c[1].length || warm_stale(c[1], stale)) continue;
|
||||
t = [WarmTask new]; t->kind = [c[0] isEqualToString:@"S"] ? 0 : 2; t->key = c[1]; t->stage = t->kind ? 5 : c[2].intValue; [tasks addObject:t];
|
||||
} else if ([c[0] isEqualToString:@"R"] && c.count >= 2) {
|
||||
pso_skey sk; if (pso_skey_parse(c[1].UTF8String, &sk)) continue;
|
||||
NSData *kd = [NSData dataWithBytes:&sk length:sizeof sk]; if ([seen_k containsObject:kd]) continue;
|
||||
[seen_k addObject:kd];
|
||||
NSString *vkey = warm_key(sk.vs, sizeof sk.vs), *fkey = warm_key(sk.fs, sizeof sk.fs);
|
||||
if (!vkey.length || warm_stale(vkey, stale) || (fkey.length && warm_stale(fkey, stale))) continue;
|
||||
NSString *pk = [NSString stringWithFormat:@"%@|%@", vkey, fkey];
|
||||
t = pairs[pk];
|
||||
if (!t) { t = [WarmTask new]; t->kind = 1; t->vkey = vkey; t->fkey = fkey; t->rs = [NSMutableArray array]; pairs[pk] = t; [tasks addObject:t]; }
|
||||
[t->rs addObject:kd];
|
||||
} else continue;
|
||||
n++;
|
||||
}
|
||||
MLOG("pipeline prewarm done: shaders %llu / render pipelines %llu (specialized fragment skipped %llu) / compute pipelines %llu, background time %.1f ms; shader keys not in the shader map (another game version) %llu",
|
||||
g_warm_shaders, g_warm_rps, g_warm_skip_fz, g_warm_cps, (bp_now() - g_warm_t0) / 1e6, g_warm_skip_stale);
|
||||
if (g_comp4) MLOG("pipeline bases so far: %llu compiled, %llu from the archive, %llu reused (%.1f ms for the bases); %llu specializations, %llu pipelines without a base (MTLDevice)",
|
||||
(unsigned long long)g_pb_built, (unsigned long long)g_pb_archived, (unsigned long long)g_pb_reused, g_pb_base_ns / 1e6, (unsigned long long)g_pb_spec, (unsigned long long)g_pb_legacy);
|
||||
}
|
||||
*lines = n; return tasks;
|
||||
}
|
||||
static void warm_task(WarmTask *t) {
|
||||
@autoreleasepool {
|
||||
if (t->kind == 0) {
|
||||
NvShader *sh = warm_shader(t->key, t->stage);
|
||||
if (sh) { WarmItem *item = [WarmItem new]; item->kind = 0; item->key = t->key; item->shader = sh; warm_enqueue(item); g_warm_shaders++; }
|
||||
g_warm_done++; return;
|
||||
}
|
||||
if (t->kind == 2) {
|
||||
NvShader *cs = warm_shader(t->key, 5); NSError *e = nil; id ps = cs ? [g_dev newComputePipelineStateWithFunction:cs->fn error:&e] : nil;
|
||||
if (cs && !ps) MLOG("[RED] background compute pipeline creation failed: %s", e.localizedDescription.UTF8String);
|
||||
if (ps) { WarmItem *item = [WarmItem new]; item->kind = 2; item->key = t->key; item->shader = cs; item->object = ps; warm_enqueue(item); g_warm_cps++; }
|
||||
g_warm_done++; return;
|
||||
}
|
||||
pso_skey sk; [t->rs[0] getBytes:&sk length:sizeof sk];
|
||||
NvShader *vs = warm_shader(t->vkey, sk.vs_stage), *fs = t->fkey.length ? warm_shader(t->fkey, sk.fs_stage) : nil;
|
||||
for (NSData *row in t->rs) {
|
||||
[row getBytes:&sk length:sizeof sk];
|
||||
if (!vs || (t->fkey.length && !fs)) { g_warm_done++; continue; }
|
||||
id<MTLFunction> fsfn = nil;
|
||||
if (sk.fz) {
|
||||
if (sk.fz_desc.kind == 1 && fs) { int fx[32]; for (int i = 0; i < 32; i++) fx[i] = sk.fz_desc.fx[i]; fsfn = bf_fn(fs, fx, sk.fz_desc.fbmask, sk.k.lmap); }
|
||||
if (!fsfn) { g_warm_skip_fz++; g_warm_done++; continue; }
|
||||
}
|
||||
pso_key k = sk.k; k.vs = (__bridge void *)vs; k.fs = (__bridge void *)fs; k.fsfn = (__bridge void *)fsfn; // pso_skey_parse normalized rows written before the key was
|
||||
NSError *e = nil; id ps = pso_build(pso_desc(&k, vs, fs, fsfn, sk.merged), &e);
|
||||
if (!ps && !vs->layer_out && [e.localizedDescription containsString:@"render_target_array_index"]) { // a vertex shader that writes the layer (see pso_get_)
|
||||
vs->layer_out = 1; e = nil; ps = pso_build(pso_desc(&k, vs, fs, fsfn, sk.merged), &e); }
|
||||
g_warm_done++;
|
||||
if (!ps) { MLOG("[RED] background render pipeline creation failed: %s", e.localizedDescription.UTF8String); continue; }
|
||||
WarmItem *item = [WarmItem new]; item->kind = 1; item->vs_key = t->vkey; item->fs_key = t->fkey; item->vs = vs; item->fs = fs; item->fsfn = fsfn; item->pso = k; item->object = ps;
|
||||
warm_enqueue(item); g_warm_rps++;
|
||||
}
|
||||
}
|
||||
}
|
||||
static void warm_finish(void) {
|
||||
MLOG("pipeline prewarm done: shaders %llu / render pipelines %llu (specialized fragment skipped %llu) / compute pipelines %llu, background time %.1f ms; shader keys not in the shader map (another game version) %llu",
|
||||
(unsigned long long)g_warm_shaders, (unsigned long long)g_warm_rps, (unsigned long long)g_warm_skip_fz, (unsigned long long)g_warm_cps, (bp_now() - g_warm_t0) / 1e6, (unsigned long long)g_warm_skip_stale);
|
||||
if (g_comp4) MLOG("pipeline bases so far: %llu compiled, %llu from the archive, %llu reused (%.1f ms for the bases); %llu specializations, %llu pipelines without a base (MTLDevice)",
|
||||
(unsigned long long)g_pb_built, (unsigned long long)g_pb_archived, (unsigned long long)g_pb_reused, g_pb_base_ns / 1e6, (unsigned long long)g_pb_spec, (unsigned long long)g_pb_legacy);
|
||||
atomic_store(&g_warm_running, 0);
|
||||
arch_flush_now(); // flush and finish compaction together, after the prewarm stops recapturing bases
|
||||
@autoreleasepool { pso_prebuild_bg(); } // then, once, the bases every shader pair's hybrid draws need
|
||||
@@ -5134,6 +5166,48 @@ int mtl_warm_wait(int timeout_ms) {
|
||||
while (atomic_load(&g_warm_running) && (bp_now() - t0) / 1000000ull < (uint64_t)timeout_ms) usleep(2000);
|
||||
return (int)((bp_now() - t0) / 1000000ull) + 1;
|
||||
}
|
||||
// ---- The prepare step: the prewarm before the game's first frame, with a progress screen ----
|
||||
// With a window (or DM_PSO_PREPARE=1), the first submission waits until every line of the prewarm lists (the profile's pso_manifest.tsv, then this device's pso_user.tsv) is built:
|
||||
// the shaders loaded and the pipelines made, on as many workers as the device has cores, at user-initiated QoS, since the game draws nothing meanwhile. It runs at the first
|
||||
// submission and not before the game starts because the vertex entry every pipeline is built with follows the game's depth mode (nvnDeviceSetDepthMode), which is known only by then.
|
||||
// Each start runs it: when everything is in the Metal shader cache and the pipeline archive it takes well under a second, and only past 0.5 s (with 0.3 s or more still to go) does the front end show its screen
|
||||
// ("Preparing shaders", a bar and a count: front_progress). A stamp that says the step ran would skip it exactly when the system dropped the Metal cache (an OS update, storage
|
||||
// pressure on iOS), which is when it is needed. DM_PSO_PREPARE=0: the prewarm runs in the background as before (as it does without a window); DM_PSO_PREPARE_THREADS=<n> sets the workers
|
||||
void front_progress(const char *title, long done, long total);
|
||||
__attribute__((weak)) void front_progress(const char *title, long done, long total) { (void)title; (void)done; (void)total; } // the front ends' (front.h); none in the unit tests
|
||||
static int prepare_on(void) {
|
||||
static int v = -1;
|
||||
if (v < 0) { const char *e = getenv("DM_PSO_PREPARE");
|
||||
if (e && !strcmp(e, "0")) { MLOG("DM_PSO_PREPARE=0: the pipeline prewarm runs in the background, the game does not wait for it"); v = 0; }
|
||||
else v = (e && !strcmp(e, "1")) || front_metal_layer() != NULL; }
|
||||
return v;
|
||||
}
|
||||
static int prepare_workers(void) {
|
||||
const char *e = getenv("DM_PSO_PREPARE_THREADS"); int n = e ? atoi(e) : 0;
|
||||
if (n <= 0) { size_t sz = sizeof n; if (sysctlbyname("hw.activecpu", &n, &sz, NULL, 0) || n < 2) n = 2; }
|
||||
return n;
|
||||
}
|
||||
static void warm_prepare(NSArray<WarmTask *> *tasks, long lines) {
|
||||
int workers = prepare_workers(); long n = (long)tasks.count;
|
||||
uint64_t t0 = bp_now(), built0 = g_pb_built, arch0 = g_pb_archived, reused0 = g_pb_reused, spec0 = g_pb_spec;
|
||||
__block _Atomic long next = 0;
|
||||
dispatch_group_t g = dispatch_group_create();
|
||||
for (int w = 0; w < workers; w++) dispatch_group_async(g, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
||||
for (;;) { long i = atomic_fetch_add(&next, 1); if (i >= n) break; warm_task(tasks[(NSUInteger)i]); }
|
||||
});
|
||||
int shown = 0;
|
||||
while (dispatch_group_wait(g, dispatch_time(DISPATCH_TIME_NOW, 50 * (int64_t)NSEC_PER_MSEC))) {
|
||||
// the screen shows past 0.5 s, unless the rate so far says the rest takes less than 0.3 s more (no screen that flashes up and goes)
|
||||
uint64_t el = bp_now() - t0; long done = g_warm_done; double rest = done ? (double)el * (double)(lines - done) / (double)done : 1e18;
|
||||
if (!shown && el >= 500000000ull && rest >= 3e8) { shown = 1; MLOG("pipeline prepare: %ld of %ld lines done after %.1f s, the progress screen shows", done, lines, el / 1e9); }
|
||||
if (shown) front_progress("Preparing shaders", g_warm_done, lines);
|
||||
}
|
||||
if (shown) front_progress(NULL, lines, lines);
|
||||
MLOG("pipeline prepare: %ld lines (%llu shaders, %llu render pipelines, %llu compute pipelines) in %.2f s on %d workers before the first frame, progress screen %s; "
|
||||
"bases %llu compiled, %llu from the archive, %llu reused; %llu specializations",
|
||||
lines, (unsigned long long)g_warm_shaders, (unsigned long long)g_warm_rps, (unsigned long long)g_warm_cps, (bp_now() - t0) / 1e9, workers, shown ? "shown" : "not shown",
|
||||
(unsigned long long)(g_pb_built - built0), (unsigned long long)(g_pb_archived - arch0), (unsigned long long)(g_pb_reused - reused0), (unsigned long long)(g_pb_spec - spec0));
|
||||
}
|
||||
static void warm_start(void) {
|
||||
if (g_warm_started++) return;
|
||||
const char *e = getenv("DM_PSO_MANIFEST");
|
||||
@@ -5151,12 +5225,20 @@ static void warm_start(void) {
|
||||
paths = all;
|
||||
}
|
||||
if (!g_warm_items) g_warm_items = [NSMutableArray array];
|
||||
g_warm_dict = [NSMutableDictionary dictionary];
|
||||
pso4_init(); // the pipeline archive opens before the prewarm builds from it
|
||||
g_warm_t0 = bp_now();
|
||||
atomic_store(&g_warm_running, 1);
|
||||
if (prepare_on()) {
|
||||
long lines = 0; NSArray<WarmTask *> *tasks = warm_parse(paths, &lines);
|
||||
if (lines) warm_prepare(tasks, lines);
|
||||
else MLOG("pipeline prepare: the prewarm lists hold nothing to build");
|
||||
warm_finish();
|
||||
return;
|
||||
}
|
||||
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); // Not optional background work: it decides whether the pipelines are ready before the player enters a run
|
||||
dispatch_queue_t q = dispatch_queue_create("dm.nvn.pso-warm", attr);
|
||||
atomic_store(&g_warm_running, 1);
|
||||
dispatch_async(q, ^{ warm_worker(paths); });
|
||||
dispatch_async(q, ^{ @autoreleasepool { long lines = 0; for (WarmTask *t in warm_parse(paths, &lines)) warm_task(t); warm_finish(); } });
|
||||
}
|
||||
static id<MTLDepthStencilState> dss_get(int has_depth, int has_stencil) {
|
||||
struct { ds_st ds; uint32_t vm[2], wm[2]; int hd, hs; } k;
|
||||
|
||||
@@ -111,7 +111,8 @@ static void scenario(NSString *dir, int merged) {
|
||||
EXPECT("seed manifest written", 1, [seed writeToFile:a atomically:YES encoding:NSUTF8StringEncoding error:nil]);
|
||||
EXPECT("user manifest written", 1, [user writeToFile:b atomically:YES encoding:NSUTF8StringEncoding error:nil]);
|
||||
uint64_t before = g_pb_spec + g_pb_legacy; g_warm_t0 = bp_now(); atomic_store(&g_warm_running, 1);
|
||||
warm_worker(@[a, b]);
|
||||
long lines = 0; for (WarmTask *t in warm_parse(@[a, b], &lines)) warm_task(t);
|
||||
warm_finish();
|
||||
EXPECT("only three effective pipelines compiled across both manifests", 3, g_pb_spec + g_pb_legacy - before);
|
||||
EXPECT("only three render pipelines enqueued", 3, g_warm_rps);
|
||||
warm_claim();
|
||||
|
||||
@@ -9,6 +9,10 @@ int front_screen_max_fps(void);
|
||||
// The frame rate the display link paces the game at (60, 30, 120; nvn_mtl.m link_sync): in fullscreen, a screen whose refresh rate is not a multiple of it is switched, while the game runs,
|
||||
// to a mode of the same size whose rate is, and back when the window leaves fullscreen or that screen (front_mac.m; none on iOS). Called from the present thread
|
||||
void front_screen_match(int fps);
|
||||
// Progress screen of the first-start prepare step (nvn_mtl.m warm_prepare: "Preparing shaders" while the pipelines of the prewarm lists are built before the game's first frame):
|
||||
// title and done of total shown over the game's view (a dark screen, the title, a bar and a count), updated on each call; title NULL hides it. Any thread; the front end switches to its main
|
||||
// thread and does not block. Nothing is shown without a window
|
||||
void front_progress(const char *title, long done, long total);
|
||||
#include <stdint.h>
|
||||
// Text input box (for the software keyboard swkbd): the main thread shows a native input box and the calling thread blocks until the user confirms / cancels.
|
||||
// All text is UTF-8; max_chars is the maximum character count (0 = unlimited, only a hint for the front end; truncation is done uniformly by the caller hle_swkbd.c in UTF-16 units).
|
||||
|
||||
@@ -293,6 +293,36 @@ void front_run(void (*guest)(void)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Progress screen of the prepare step (front.h front_progress): a dark view over the game's, the title, a bar and a count, centered; AppKit views, so it draws while the game's thread waits
|
||||
static NSView *g_prog; static NSTextField *g_prog_title, *g_prog_count; static NSProgressIndicator *g_prog_bar;
|
||||
static NSTextField *prog_label(NSRect r, CGFloat size, NSFontWeight w, NSColor *c) {
|
||||
NSTextField *t = [NSTextField labelWithString:@""]; t.frame = r; t.alignment = NSTextAlignmentCenter; t.textColor = c;
|
||||
t.font = [NSFont monospacedDigitSystemFontOfSize:size weight:w]; return t;
|
||||
}
|
||||
void front_progress(const char *title, long done, long total) {
|
||||
if (!g_layer) return;
|
||||
NSString *tt = title ? @(title) : nil;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!tt) { [g_prog removeFromSuperview]; g_prog = nil; return; }
|
||||
if (!g_prog) {
|
||||
NSView *cv = g_win.contentView;
|
||||
g_prog = [[NSView alloc] initWithFrame:cv.bounds]; g_prog.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
g_prog.wantsLayer = YES; g_prog.layer.backgroundColor = [NSColor colorWithWhite:0.06 alpha:0.94].CGColor;
|
||||
NSView *box = [[NSView alloc] initWithFrame:NSMakeRect(floor((cv.bounds.size.width - 400) / 2), floor((cv.bounds.size.height - 96) / 2), 400, 96)];
|
||||
box.autoresizingMask = NSViewMinXMargin | NSViewMaxXMargin | NSViewMinYMargin | NSViewMaxYMargin;
|
||||
g_prog_title = prog_label(NSMakeRect(0, 62, 400, 30), 20, NSFontWeightSemibold, NSColor.whiteColor);
|
||||
g_prog_bar = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(20, 36, 360, 20)];
|
||||
g_prog_bar.style = NSProgressIndicatorStyleBar; g_prog_bar.indeterminate = NO; g_prog_bar.minValue = 0;
|
||||
g_prog_count = prog_label(NSMakeRect(0, 4, 400, 22), 13, NSFontWeightRegular, [NSColor colorWithWhite:0.72 alpha:1]);
|
||||
[box addSubview:g_prog_title]; [box addSubview:g_prog_bar]; [box addSubview:g_prog_count]; [g_prog addSubview:box];
|
||||
[cv addSubview:g_prog];
|
||||
}
|
||||
g_prog_title.stringValue = tt; g_prog_bar.maxValue = total > 0 ? (double)total : 1; g_prog_bar.doubleValue = (double)done;
|
||||
g_prog_count.stringValue = [NSString stringWithFormat:@"%@ of %@", [NSNumberFormatter localizedStringFromNumber:@(done) numberStyle:NSNumberFormatterDecimalStyle],
|
||||
[NSNumberFormatter localizedStringFromNumber:@(total) numberStyle:NSNumberFormatterDecimalStyle]];
|
||||
});
|
||||
}
|
||||
|
||||
// Software keyboard input box (front.h): the main thread shows an NSAlert + text field and the guest thread waits synchronously for the result. Not shown in windowless mode (returns -1, the caller treats it as cancel)
|
||||
extern int g_swkbd_min_chars; // hle_swkbd.c: this input's minimum length from ShowKeyboardArg; the applet keeps OK disabled until the text is long enough
|
||||
int front_text_input(const char *title, const char *sub, const char *guide, const char *initial, int max_chars, char *out, int out_cap) {
|
||||
|
||||
@@ -142,6 +142,32 @@ static UILabel *front_ios_fps_label(void) {
|
||||
return l;
|
||||
}
|
||||
|
||||
// Progress screen of the prepare step (front.h front_progress): a dark view over the game's view and the pad, the title, a bar and a count, centered; UIKit views, so it draws while the game's thread waits
|
||||
static UIView *g_prog; static UILabel *g_prog_count; static UIProgressView *g_prog_bar; static UILabel *g_prog_title;
|
||||
void front_progress(const char *title, long done, long total) {
|
||||
if (!g_layer) return;
|
||||
NSString *tt = title ? @(title) : nil;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!tt) { [g_prog removeFromSuperview]; g_prog = nil; return; }
|
||||
if (!g_prog) {
|
||||
UIView *root = g_root.view; if (!root) return;
|
||||
g_prog = [[UIView alloc] initWithFrame:root.bounds]; g_prog.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
g_prog.backgroundColor = [UIColor colorWithWhite:0.06 alpha:0.94];
|
||||
UIView *box = [[UIView alloc] initWithFrame:CGRectMake(floor((root.bounds.size.width - 320) / 2), floor((root.bounds.size.height - 96) / 2), 320, 96)];
|
||||
box.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
|
||||
g_prog_title = [[UILabel alloc] initWithFrame:CGRectMake(0, 4, 320, 30)]; g_prog_title.textAlignment = NSTextAlignmentCenter; g_prog_title.textColor = UIColor.whiteColor;
|
||||
g_prog_title.font = [UIFont systemFontOfSize:20 weight:UIFontWeightSemibold];
|
||||
g_prog_bar = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault]; g_prog_bar.frame = CGRectMake(20, 48, 280, 4);
|
||||
g_prog_count = [[UILabel alloc] initWithFrame:CGRectMake(0, 64, 320, 22)]; g_prog_count.textAlignment = NSTextAlignmentCenter;
|
||||
g_prog_count.textColor = [UIColor colorWithWhite:0.72 alpha:1]; g_prog_count.font = [UIFont monospacedDigitSystemFontOfSize:13 weight:UIFontWeightRegular];
|
||||
[box addSubview:g_prog_title]; [box addSubview:g_prog_bar]; [box addSubview:g_prog_count]; [g_prog addSubview:box];
|
||||
[root addSubview:g_prog];
|
||||
}
|
||||
g_prog_title.text = tt; g_prog_bar.progress = total > 0 ? (float)done / (float)total : 0;
|
||||
g_prog_count.text = [NSString stringWithFormat:@"%@ of %@", [NSNumberFormatter localizedStringFromNumber:@(done) numberStyle:NSNumberFormatterDecimalStyle],
|
||||
[NSNumberFormatter localizedStringFromNumber:@(total) numberStyle:NSNumberFormatterDecimalStyle]];
|
||||
});
|
||||
}
|
||||
void *front_metal_layer(void) { return (__bridge void *)g_layer; }
|
||||
void front_run(void (*guest)(void)) { guest(); } // Already on main.m's big-stack thread
|
||||
|
||||
|
||||
Reference in new issue
Block a user