diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index bec1647..27e0b28 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -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 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 *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 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 *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 *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 *stale) { + NSString *mk = [key hasSuffix:@"_b"] ? [key substringToIndex:key.length - 2] : key; // library names are _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 _b - static NSMutableSet *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 *paths) { - @autoreleasepool { - NSMutableDictionary *dict = [NSMutableDictionary dictionary]; - NSMutableSet *seen = [NSMutableSet set]; - NSMutableSet *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 *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 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 *warm_parse(NSArray *paths, long *lines) { + NSMutableArray *tasks = [NSMutableArray array]; NSMutableDictionary *pairs = [NSMutableDictionary dictionary]; + NSMutableSet *seen = [NSMutableSet set], *stale = [NSMutableSet set]; long n = 0; + NSMutableSet *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 *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 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= 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 *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 *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 dss_get(int has_depth, int has_stencil) { struct { ds_st ds; uint32_t vm[2], wm[2]; int hd, hs; } k; diff --git a/dm/gfx/tests/t_pso_warm.m b/dm/gfx/tests/t_pso_warm.m index 39f596c..2dc1f40 100644 --- a/dm/gfx/tests/t_pso_warm.m +++ b/dm/gfx/tests/t_pso_warm.m @@ -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(); diff --git a/dm/host/front.h b/dm/host/front.h index 5b7b49b..98c42ff 100644 --- a/dm/host/front.h +++ b/dm/host/front.h @@ -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 // 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). diff --git a/dm/host/front_mac.m b/dm/host/front_mac.m index 8ef0e43..2181e33 100644 --- a/dm/host/front_mac.m +++ b/dm/host/front_mac.m @@ -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) { diff --git a/dm/ios/Sources/front_ios.m b/dm/ios/Sources/front_ios.m index d01c66d..3c214e7 100644 --- a/dm/ios/Sources/front_ios.m +++ b/dm/ios/Sources/front_ios.m @@ -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