From 5a81e13f24593df904872f58c7adc98a9ff978d2 Mon Sep 17 00:00:00 2001 From: Aleksandr Svidchenkov Date: Sun, 25 Oct 2026 20:18:26 +0400 Subject: [PATCH 1/6] NVN on Metal: no autorelease in ReadTexels and arena_alloc, and a registry lock that lends priority (general, from MacNativeGames) - mtl_read_texels runs on the game's own thread outside a submission, where no pool drains what Metal autoreleases (the command buffer, the blit encoder, the read-back buffer), and that thread lives for good: every GPU read-back stayed alive. It runs under an @autoreleasepool now. MacNativeGames 635fb31 measured Breath of the Wild's ~130 read-backs a second holding 3.7 GB of GPU memory after 30 minutes. - arena_alloc's buffer out-parameter is __strong: an unqualified id * is __autoreleasing under ARC, which put the arena buffer in the autorelease pool at every call (a few per draw). The arena also keeps its length, so an allocation, arena_new and the idle trim send no message. MacNativeGames 0531f28: autorelease 2.63% -> 1.73% of replay time. - The texture / sampler registry lock g_rmu is an os_unfair_lock, which lends a waiter's priority to its owner. Game threads (priority 46) take it to register textures and samplers, the submit thread (97) for every argument encode; preempted while holding the mutex, they kept the submit thread waiting 1.8 ms a frame in a Tears of the Kingdom shrine (MacNativeGames 5f5de5e: 155 -> ~0 ms/s of waiting, frame time unchanged there). t_stencil_view, t_view_levels and t_view_swizzle take the lock the same way. --- dm/gfx/nvn/nvn_mtl.m | 63 ++++++++++++++++++++--------------- dm/gfx/tests/t_stencil_view.m | 2 +- dm/gfx/tests/t_view_levels.m | 4 +-- dm/gfx/tests/t_view_swizzle.m | 10 +++--- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index 70c1056..f129f1b 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -919,7 +919,7 @@ static id g_ubs_buf; static size_t g_ubs_off, g_ubs_size; // made of them alone, latest first per byte (exactly what the GPU would read, whatever earlier frames still have in flight); when they cover only part of it, the rest comes from memory (ubp_get). // The real copies are encoded in order when the pass ends (rp_end), so later passes and the CPU see them as before. The pass still ends before a draw that would need more: a partly covered binding // whose rest memory cannot give, or a read of the deferred data through a vertex, index, indirect or storage buffer (ubd_conflict). Off in TBDR mode (it reorders passes). DM_NVN_UB_DEFER=0 turns it off -static size_t arena_alloc(size_t n, id *buf); static inline void args_dirty_all(void); static void ubd_dirty(uint64_t lo, uint64_t hi); +static size_t arena_alloc(size_t n, __strong id *buf); static inline void args_dirty_all(void); static void ubd_dirty(uint64_t lo, uint64_t hi); typedef struct { uint64_t lo, hi, gpu; size_t off; __unsafe_unretained id buf; } ubd_ent; // data at buf + off (the ubs ring; a ring block replaced meanwhile lives until this submission completes) enum { UBD_MAX = 512 }; static ubd_ent g_ubd[UBD_MAX]; static int g_nubd; @@ -2897,7 +2897,10 @@ static int clear_rect_zero(int i) { // ================= Shaders / descriptor pool / pipelines / draws ================= -static pthread_mutex_t g_rmu = PTHREAD_MUTEX_INITIALIZER; +// The texture / sampler registry lock: game threads take it to register textures and samplers, the submit thread for every argument encode. An os_unfair_lock lends a +// waiter's priority to its owner: with a mutex, a game thread (priority 46) preempted while holding it kept the submit thread (97) waiting, 1.8 ms a frame in a Tears of +// the Kingdom shrine (MacNativeGames 5f5de5e: 155 -> ~0 ms/s of waiting) +static os_unfair_lock g_rmu = OS_UNFAIR_LOCK_INIT; // ---------- Shader mapping table: Switch control section hash → PC metallib (produced by dm/gfx/tools/shader_map.py) ---------- static NSMutableDictionary *g_shmap; static NSString *g_shmap_path, *g_msl_dir; @@ -3372,8 +3375,8 @@ static void vc_put(uint64_t ser, const void *pool, const vc_key *k, idview[i]) { defer_obj_unref((__bridge id)s->view[i]); CFRelease(s->view[i]); } s->k[i] = *k; s->view[i] = (void *)CFBridgingRetain(v); g_vc_new++; } -void mtl_vc_drop(uint64_t ser) { pthread_mutex_lock(&g_rmu); vc_set *s = nvn_hm_del(&g_vc, &ser); if (s) vc_set_free(s); pthread_mutex_unlock(&g_rmu); } -void mtl_vc_stats(unsigned *n, uint64_t *hit, uint64_t *made) { pthread_mutex_lock(&g_rmu); *n = g_vc.n; *hit = g_vc_hit; *made = g_vc_new; pthread_mutex_unlock(&g_rmu); } +void mtl_vc_drop(uint64_t ser) { os_unfair_lock_lock(&g_rmu); vc_set *s = nvn_hm_del(&g_vc, &ser); if (s) vc_set_free(s); os_unfair_lock_unlock(&g_rmu); } +void mtl_vc_stats(unsigned *n, uint64_t *hit, uint64_t *made) { os_unfair_lock_lock(&g_rmu); *n = g_vc.n; *hit = g_vc_hit; *made = g_vc_new; os_unfair_lock_unlock(&g_rmu); } static void vc_drop_pool(const void *pool) { // caller holds g_rmu for (uint32_t i = 0; i < g_vc.cap;) { // a deletion shifts later entries back into slot i: look at it again vc_set *s = g_vc.vals[i]; @@ -3414,16 +3417,16 @@ static id tbuf_as_2d(int tid, id t, uint64_t *rid) { } void mtl_tb2_stats(uint64_t *binds, uint64_t *made, uint64_t *fail) { *binds = g_tb2_binds; *made = g_tb2_made; *fail = g_tb2_fail; } static void treg_drop_pool_views(const void *pool) { - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); vc_drop_pool(pool); for (int i = 0; i < g_ntreg; i++) if (g_treg[i].view && g_treg[i].ok && (const void *)g_treg[i].t.pool == pool) { defer_obj_unref((__bridge id)g_treg[i].view); CFRelease(g_treg[i].view); g_treg[i].view = NULL; g_treg[i].view_id = g_treg[i].view_rid = 0; treg_drop_ar(&g_treg[i]); } - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } void mtl_reg_tex(int tid, const tex_t *t, const view_t *v) { int id_ = tid; if (id_ < 0) return; - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); if (id_ >= g_ntreg) { int n = id_ + 1024; g_treg = realloc(g_treg, n * sizeof *g_treg); memset(g_treg + g_ntreg, 0, (n - g_ntreg) * sizeof *g_treg); g_ntreg = n; } // Registering what the ID already holds changes nothing: its view and the registration generation stay (the lookup still checks the base texture). About 1 in 6 of the // registrations KTGL (Age of Calamity) makes before every bind repeat the ID's texture; the rest cycle textures through its IDs (the view cache below, vc_get) @@ -3437,7 +3440,7 @@ void mtl_reg_tex(int tid, const tex_t *t, const view_t *v) { rr->ok = t != NULL; if (t) rr->t = *t; rr->has_v = v != NULL; if (v) rr->v = *v; if (id_ < (1 << 20)) atomic_store_explicit(&g_tser[id_], t ? t->serial : 0, memory_order_release); } - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); { // DM_NVN_TREG_ID=n logs every registration of texture ID n: what it holds from then on, and when (the frame being recorded) static int want = -2; if (want == -2) { const char *e = getenv("DM_NVN_TREG_ID"); want = e ? atoi(e) : -1; } if (want == tid) MLOG("TREG id %d frame %llu %s ser=%llu %ux%u format=%#x target %d view %d", tid, (unsigned long long)atomic_load(&g_cur_frame), same ? "(the same)" : "→", @@ -3454,13 +3457,13 @@ void mtl_reg_tex(int tid, const tex_t *t, const view_t *v) { } void mtl_reg_samp(int sid, const samp_par *sp) { int id_ = sid; if (id_ < 0) return; - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); if (id_ >= g_nsreg) { int n = id_ + 256; g_sreg = realloc(g_sreg, n * sizeof *g_sreg); g_sreg_ok = realloc(g_sreg_ok, n); g_sview = realloc(g_sview, n * sizeof *g_sview); g_srid = realloc(g_srid, n * sizeof *g_srid); memset(g_sreg_ok + g_nsreg, 0, n - g_nsreg); memset(g_sview + g_nsreg, 0, (n - g_nsreg) * sizeof *g_sview); memset(g_srid + g_nsreg, 0, (n - g_nsreg) * sizeof *g_srid); g_nsreg = n; } if (g_sview[id_]) { CFRelease(g_sview[id_]); g_sview[id_] = NULL; g_srid[id_] = 0; } g_reg_gen++; g_sreg[id_] = *sp; g_sreg_ok[id_] = 1; - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } // Handles of deferred binds (BindTexturesDeferred) are read at execution: the record keeps the address of the handles (see hle_nvn.c); every reader of a bind record refreshes it first. Command memory is writable, so it is updated in place static inline void bh_fresh(const p_bind_handles *b) { @@ -3667,13 +3670,13 @@ static int mtl_pf_dpf(MTLPixelFormat pf) { // shader's class; a texture made without the format-view usage is upgraded before the next draw pass (img_fit_upgrade), three textures in that game. Caller holds g_rmu static int g_imfit_pend[16]; static void img_fit_upgrade(void) { - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); for (int i = 0; i < g_imfit_npend; i++) { int tid = g_imfit_pend[i]; if (tid <= 0 || tid >= g_ntreg || !g_treg[tid].ok) continue; id b = tex_mtl(&g_treg[tid].t); if (b.parentTexture) b = b.parentTexture; if (b && !(b.usage & MTLTextureUsagePixelFormatView)) tex_upgrade_pfv(&g_treg[tid].t, b, MTLPixelFormatR32Uint); } g_imfit_npend = 0; - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } // The same view, in the texture's own class, for a swizzled view (the channel mapping of tex_by_id): Metal gives a swizzled view only the read usage (a base of usage 0x7, a view of 0x1), // so an RGBX8 or RGB565 image was bound as the dummy and its writes were lost. An image takes no channel mapping, as a Vulkan storage image view must have the identity swizzle @@ -3742,17 +3745,17 @@ static inline __attribute__((always_inline)) void *samp_fast(int sid) { } // ---------- Argument buffer blocks: one block per submission, reclaimed after the GPU completes ---------- -@interface NvArena : NSObject { @public id buf; uint64_t gpu, fence; size_t used; } @end +@interface NvArena : NSObject { @public id buf; uint64_t gpu, fence; size_t used, len; } @end // len: buf.length, kept so an allocation sends no message @implementation NvArena @end static NSMutableArray *g_arenas; static NvArena *g_cur_arena; static NSMutableArray *g_sub_arenas; static NvArena *arena_new(size_t minimum) { // The for-in loop variable does not retain the object: take a strong reference first and then remove it from the array, otherwise what is returned after the array releases it is a dangling pointer NvArena *r = nil; // the smallest one that fits, so the usual small requests do not keep a large scratch arena alive (ARENA_IDLE, arena_begin) - for (NvArena *it in g_arenas) if (it->fence && it->fence <= g_done && it->buf.length >= minimum && (!r || it->buf.length < r->buf.length)) r = it; + for (NvArena *it in g_arenas) if (it->fence && it->fence <= g_done && it->len >= minimum && (!r || it->len < r->len)) r = it; if (r) { [g_arenas removeObject:r]; r->fence = 0; r->used = 0; return r; } NvArena *a = [NvArena new]; a->buf = [g_dev newBufferWithLength:MAX(minimum, 4u << 20) options:MTLResourceStorageModeShared]; if (!a->buf) MDIE("cannot allocate %zu bytes of frame scratch", minimum); - a->gpu = a->buf.gpuAddress; res_add(a->buf); g_arena_made++; return a; + a->gpu = a->buf.gpuAddress; a->len = a->buf.length; res_add(a->buf); g_arena_made++; return a; } // Arenas above ARENA_KEEP bytes are scratch for large one-off work (a geometry draw's capture, up to 256 MB each). They are kept for reuse while that work goes on and released once // none has been used for ARENA_IDLE submissions: OCTOPATH TRAVELER II draws a 129,032-triangle mesh through a geometry shader now and then in its battles (nvn_geometry.inc geo_split, @@ -3764,12 +3767,14 @@ void arena_begin(void) { g_cur_arena = nil; uint64_t done = g_done; // read as the submission start does (its allocator and visibility ring) for (NSUInteger i = g_arenas.count; i-- > 0;) { NvArena *a = g_arenas[i]; - if (a->fence && a->fence <= done && a->buf.length > ARENA_KEEP && g_fence > a->fence + ARENA_IDLE) { res_remove(a->buf); [g_arenas removeObjectAtIndex:i]; } } + if (a->fence && a->fence <= done && a->len > ARENA_KEEP && g_fence > a->fence + ARENA_IDLE) { res_remove(a->buf); [g_arenas removeObjectAtIndex:i]; } } } void arena_commit(uint64_t fence) { for (NvArena *a in g_sub_arenas) { a->fence = fence; [g_arenas addObject:a]; } [g_sub_arenas removeAllObjects]; g_cur_arena = nil; } -static size_t arena_alloc(size_t n, id *buf) { +// buf is __strong: an unqualified id * out-parameter is __autoreleasing under ARC, which put the arena buffer in the autorelease pool at every call (a few per draw; +// MacNativeGames 0531f28: autorelease 2.63% -> 1.73% of replay time) +static size_t arena_alloc(size_t n, __strong id *buf) { n = (n + 255) & ~(size_t)255; - if (!g_cur_arena || g_cur_arena->used + n > g_cur_arena->buf.length) { + if (!g_cur_arena || g_cur_arena->used + n > g_cur_arena->len) { pthread_mutex_lock(&g_mu); g_cur_arena = arena_new(n); pthread_mutex_unlock(&g_mu); [g_sub_arenas addObject:g_cur_arena]; } @@ -4962,7 +4967,7 @@ static int encode_args_(id en, NvShader *sh, int nvn_stage, i static uint64_t bl_pass[6]; static int bl_all = -1; if (bl_all < 0) bl_all = getenv("DM_NVN_PASSDUMP_ALL") != NULL; // Record on every new encode (otherwise only the first time per pass per stage) int bl = passdump() && (bl_all || g_kd_force || bl_pass[nvn_stage] != g_stat_pass); if (bl) { bl_pass[nvn_stage] = g_stat_pass; PD("K %llu st%d key=%s nb=%d", g_stat_pass, nvn_stage, sh->key[0] ? sh->key : sh->builtin ? sh->builtin : "?", sh->rf.nb); } - pthread_mutex_lock(&g_rmu); // The texture / sampler registration tables may be being grown or rewritten by the game thread + os_unfair_lock_lock(&g_rmu); // The texture / sampler registration tables may be being grown or rewritten by the game thread __unsafe_unretained id atomic_img[32] = {nil}; uint64_t atomic_rid[32] = {0}; int atomic_real[32] = {0}; const nvn_rbind *atomic_desc[32] = {0}; @@ -5218,7 +5223,7 @@ static int encode_args_(id en, NvShader *sh, int nvn_stage, i } else RED_ONCE(46, "binding kind %d not implemented", b->kind); } #undef USE - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); id ab; size_t off = arena_alloc(arg_len, &ab); memcpy((uint8_t *)ab.contents + off, slots, arg_len); bind_args(en, nvn_stage, metal_vertex, ab, g_cur_arena->gpu + off, off, sh->rf.abi); @@ -5683,7 +5688,7 @@ static int draw_samples_rt(NvProg *pr) { // Sampled attachments keep the origi for (int i = 0; i < sh->rf.nb; i++) { const nvn_rbind *b = &sh->rf.b[i]; uint64_t h; if (b->kind == NVN_RB_IMG) h = b->reg < 32 && sh->stage < 6 ? S.img[sh->stage][b->reg] : 0; else if (b->kind == NVN_RB_TEX || b->kind == NVN_RB_TBUF) h = sh->stage < 6 ? tex_slot_h(S.tex[sh->stage], S.img[sh->stage], b->reg) : 0; else continue; int tid = (int)(h & 0xfffff); if (!tid) continue; - pthread_mutex_lock(&g_rmu); uint64_t ser = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t.serial : 0; pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); uint64_t ser = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t.serial : 0; os_unfair_lock_unlock(&g_rmu); for (int j = 0; j < nr; j++) if (ser && ser == rs[j]) return 1; } } return 0; } @@ -6712,11 +6717,11 @@ static void fb_prepare(NvProg *pr) { NvShader *fs = pr->sh[1]; for (int i = 0; i < fs->rf.nb && g_fb_n < 4; i++) { const nvn_rbind *b = &fs->rf.b[i]; if (b->kind != NVN_RB_TEX || b->reg >= 128) continue; int tid = (int)(tex_slot_h(S.tex[1], S.img[1], b->reg) & 0xfffff); if (!tid) continue; - pthread_mutex_lock(&g_rmu); uint64_t ser = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t.serial : 0; pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); uint64_t ser = tid < g_ntreg && g_treg[tid].ok ? g_treg[tid].t.serial : 0; os_unfair_lock_unlock(&g_rmu); if (!ser) continue; for (int k = 0; k < R.rt.n && k < 8 && g_fb_n < 4; k++) { if (!R.rt.color[k] || R.rt.color[k]->serial != ser) continue; uint64_t aser; int lv, sl; rt_sub(&R.rt, k, &aser, &lv, &sl); - pthread_mutex_lock(&g_rmu); void *tv = NULL; tex_fast_rid(tid, &tv); id t = (__bridge id)tv; pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); void *tv = NULL; tex_fast_rid(tid, &tv); id t = (__bridge id)tv; os_unfair_lock_unlock(&g_rmu); if (!t) continue; int dup = 0; for (int q = 0; q < g_fb_n; q++) dup |= g_fb_src[q] == t; if (dup) continue; id base = t.parentTexture ? t.parentTexture : t; NSUInteger tl = t.parentTexture ? t.parentRelativeLevel : 0, ts = t.parentTexture ? t.parentRelativeSlice : 0; @@ -7624,6 +7629,9 @@ void mtl_read_texels(const tex_t *t, unsigned level, unsigned layer0, const NVNc free(lin); return; } + // The game's own thread, outside a submission: no pool drains what Metal autoreleases here (the command buffer, the encoder, the read-back buffer), and the thread lives + // for good, so without one every GPU read-back stayed alive (MacNativeGames 635fb31: Breath of the Wild's 130 read-backs a second held 3.7 GB after 30 minutes) + @autoreleasepool { id mt = tex_mtl(t); if (!mt) { memset(dst, 0, img * nz); return; } mtl_finish_gpu_at(WS_READTEX); @@ -7634,6 +7642,7 @@ void mtl_read_texels(const tex_t *t, unsigned level, unsigned layer0, const NVNc sourceSize:MTLSizeMake(r->width, r->height, 1) toBuffer:buf destinationOffset:z * wb * hb destinationBytesPerRow:wb destinationBytesPerImage:wb * hb]; enc_b_end(b); [cb commit]; [cb waitUntilCompleted]; for (int z = 0; z < nz; z++) for (size_t y = 0; y < hb; y++) memcpy(dst + z * img + y * row, (uint8_t *)buf.contents + z * wb * hb + y * wb, wb); + } } static void copy_b2t(const p_copy_b2t *c) { if (c->tex) zskip_del(c->tex->serial); @@ -8260,11 +8269,11 @@ void mtl_res_snapshot(mtl_res_count *out) { out->pools = (uint32_t)g_npool; out->pool_adds = g_pool_adds; pthread_mutex_unlock(&g_mu); - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); out->view_slots = (uint32_t)g_ntreg; for (int i = 0; i < g_ntreg; i++) if (g_treg[i].view) out->view_nonnull++; for (int i = 0; i < g_nsreg; i++) if (g_sview[i]) out->sview_nonnull++; - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } // Resource counts (read by long-run stability L3): the host calls it per game tick / event (every 250 ticks, loading screen dismissed, exit), not sampled by wall clock / present count. // Line format: [nvn] resource counts tick : ... (names contain no spaces, single-space separated); new fields are appended to the end of this line @@ -8404,11 +8413,11 @@ void dm_gfx_texraw(uint64_t serial, const char *path) { } // The Mac's DM_CTL "texlist " (hle_hid.c): the registered textures of that target (2 = 3D), with their storage (guest address, size) and block-linear layout, for "memdump" void dm_gfx_texlist(int target) { - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); for (int i = 1; i < g_ntreg; i++) { const tex_t *t = &g_treg[i].t; if (!g_treg[i].ok || t->d.target != target) continue; MLOG("[texlist] tid=%d ser=%llu target %d %ux%ux%u format %#x levels %u flags %#x storage %p size %#llx block height/depth log2 %u/%u", i, (unsigned long long)t->serial, t->d.target, t->d.width, t->d.height, t->d.depth, t->d.format, t->d.levels, t->d.flags, (void *)t->mem, (unsigned long long)t->L.size, t->L.tileH, t->L.tileD); } - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } void dm_gfx_texdump_next(const char *path) { if (g_td_frame == -2) td_note(nil, 0); // read DM_NVN_TEXDUMP first, so it does not override the request diff --git a/dm/gfx/tests/t_stencil_view.m b/dm/gfx/tests/t_stencil_view.m index b6cc0f1..ecdd70d 100644 --- a/dm/gfx/tests/t_stencil_view.m +++ b/dm/gfx/tests/t_stencil_view.m @@ -44,7 +44,7 @@ int main(void) { @autoreleasepool { // A stencil-mode view, and the texture built in stencil mode, are X32_Stencil8 views of the depth-stencil storage itself (no upgraded copy) view_t v = {.set_dsmode=1, .dsmode=NVN_TEXTURE_DEPTH_STENCIL_MODE_STENCIL}; mtl_reg_tex(800 + 2 * i, &s, &v); mtl_reg_tex(801 + 2 * i, &sb, NULL); - pthread_mutex_lock(&g_rmu); id sv = tex_by_id(800 + 2 * i), bv = tex_by_id(801 + 2 * i); pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id sv = tex_by_id(800 + 2 * i), bv = tex_by_id(801 + 2 * i); os_unfair_lock_unlock(&g_rmu); CHECK(sv.pixelFormat == MTLPixelFormatX32_Stencil8 && sv.parentTexture == base, "a stencil-mode view reads the stencil of the texture's own storage"); CHECK(bv.pixelFormat == MTLPixelFormatX32_Stencil8 && bv.parentTexture == base, "a texture built in stencil mode reads the stencil of the same storage"); CHECK(tex_get(s.serial) == base, "the depth-stencil texture is not replaced"); diff --git a/dm/gfx/tests/t_view_levels.m b/dm/gfx/tests/t_view_levels.m index 4f42e86..9439418 100644 --- a/dm/gfx/tests/t_view_levels.m +++ b/dm/gfx/tests/t_view_levels.m @@ -18,7 +18,7 @@ static void view_case(int tid, unsigned level, unsigned count, unsigned expected serial_put(t.serial, base); view_t v = {.set_levels=1, .base_level=level, .nlevels=count}; mtl_reg_tex(tid, &t, &v); - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id view = tex_by_id(tid); CHECK((view != nil) == (expected != 0), "invalid start is empty; valid start creates view"); if (expected) { @@ -27,7 +27,7 @@ static void view_case(int tid, unsigned level, unsigned count, unsigned expected CHECK(view.arrayLength == layers, "array layers preserved"); CHECK(tex_by_id(tid) == view, "cached view preserves range"); } - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); } int main(void) { @autoreleasepool { mtl_init(); diff --git a/dm/gfx/tests/t_view_swizzle.m b/dm/gfx/tests/t_view_swizzle.m index 385fff4..2ebe7d8 100644 --- a/dm/gfx/tests/t_view_swizzle.m +++ b/dm/gfx/tests/t_view_swizzle.m @@ -45,11 +45,11 @@ static void swizzle_case(int tid, int format, int view_format, int uploaded, con view_t v = {0}; if (mapping) { v.set_swizzle = 1; memcpy(v.swz, mapping, 4); } if (view_format) { v.set_format = 1; v.format = view_format; } mtl_reg_tex(tid, &t, mapping || view_format ? &v : NULL); - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id view = tex_by_id(tid); CHECK(view != nil, "registered view created"); CHECK(tex_by_id(tid) == view, "cached view retained"); - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); read_check(tid, view, expected, 0.001f); } // What Metal itself wrote into a texture of the RGB565 family is in Metal's order: a clear to red reads red, with no swap @@ -64,7 +64,7 @@ static void cleared_case(int tid, int format) { id command = q_cb(); [[command renderCommandEncoderWithDescriptor:rp] endEncoding]; [command commit]; [command waitUntilCompleted]; serial_put(t.serial, base); mtl_reg_tex(tid, &t, NULL); - pthread_mutex_lock(&g_rmu); id view = tex_by_id(tid); pthread_mutex_unlock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id view = tex_by_id(tid); os_unfair_lock_unlock(&g_rmu); CHECK(view == base, "a cleared RGB565-family texture needs no view"); read_check(tid, view, (const float[8]){1,0,0,1, 1,0,0,1}, 0.001f); } @@ -77,7 +77,7 @@ static void image_case(int tid) { CHECK(base != nil && (base.usage & MTLTextureUsageShaderWrite), "image texture created"); serial_put(t.serial, base); mtl_reg_tex(tid, &t, NULL); - pthread_mutex_lock(&g_rmu); + os_unfair_lock_lock(&g_rmu); id view = tex_by_id(tid); uint64_t rid = 0, rid2 = 0; CHECK(view != base && !(view.usage & MTLTextureUsageShaderWrite), "the sampled view is swizzled and read-only"); id img = img_fit(tid, view, NVN_DPF_FLOAT, &rid); @@ -85,7 +85,7 @@ static void image_case(int tid) { CHECK(img && (img.usage & MTLTextureUsageShaderWrite) && rid == img.gpuResourceID._impl, "the image slot gets a writable view"); CHECK(s.red == MTLTextureSwizzleRed && s.green == MTLTextureSwizzleGreen && s.blue == MTLTextureSwizzleBlue && s.alpha == MTLTextureSwizzleAlpha, "the image view has no channel mapping"); CHECK(img_fit(tid, view, NVN_DPF_FLOAT, &rid2) == img && rid2 == rid, "the image view is kept for the next bind"); - pthread_mutex_unlock(&g_rmu); + os_unfair_lock_unlock(&g_rmu); run2(pipe_of(@"#include \nusing namespace metal;\nkernel void store(texture2d t [[texture(0)]], uint i [[thread_position_in_grid]]) { t.write(float4(0.25, 0.5, 0.75, 0.125), uint2(i, 0)); }\n", @"store"), img, nil); uint8_t px[8]; [base getBytes:px bytesPerRow:8 fromRegion:MTLRegionMake2D(0, 0, 2, 1) mipmapLevel:0]; for (int i = 0; i < 8; i++) CHECK(abs((int)px[i] - (int[4]){64, 128, 191, 32}[i % 4]) <= 1, "the image write is stored as written"); From 7dc250e2f1f0f698f04394be78b8c99d3fd77831 Mon Sep 17 00:00:00 2001 From: seedds Date: Mon, 26 Oct 2026 01:05:41 +0800 Subject: [PATCH 2/6] Bound NVN upload staging and retire it on upload completion Split GPU upload batches at 128 MiB and retire overflow and CPU staging when the upload completes, rather than retaining it until the main rendering fence. Scope weak-reference diagnostics to avoid temporary staging retention. Add a real-Metal regression covering memory bounds, upload pixels and an open render pass in direct and deferred encoding modes. Both modes pass 261 checks; documentation checks and the TotK host build pass. Refs #147. Reporter-device validation and the full-device reboot investigation remain open. --- dm/games/0100F2C0115B6000/README.md | 3 ++ dm/gfx/nvn/nvn_mtl.m | 38 +++++++++++++--- dm/gfx/tests/.gitignore | 2 + dm/gfx/tests/Makefile | 7 +++ dm/gfx/tests/t_upload_budget.m | 69 +++++++++++++++++++++++++++++ docs/agents/symptoms.md | 1 + docs/reference/memory.md | 8 ++++ 7 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 dm/gfx/tests/t_upload_budget.m diff --git a/dm/games/0100F2C0115B6000/README.md b/dm/games/0100F2C0115B6000/README.md index b84565f..a1840a2 100644 --- a/dm/games/0100F2C0115B6000/README.md +++ b/dm/games/0100F2C0115B6000/README.md @@ -26,6 +26,7 @@ Update 1.4.3 (v720896) over the base game, SDK 20.5.22. Nintendo EPD's engine (s ## Memory +- **Current (issue #147): confirmed transient upload-memory bug on the Mac; iOS validation pending.** With update 1.4.3, the corrected issue #70 SDK module, shader chain `dec5ee141a9f` and 30 fps, walking from the Great Sky Island save produced 10.70 GiB peak footprint after about 474 s; a shorter repeat reached 12.81 GiB after about 150 s. Live staging peaked at 4.48 / 5.82 GiB and the open batch at 4.41 / 5.75 GiB, then memory fell back near 4 GiB. The shorter burst logged 3,084.2 MB of uploads in one frame. A temporary 50 ms sampler and the kernel footprint-peak ledger caught peaks missed by the periodic reports and the 8 GiB sampled watchdog. With [bounded batches and upload-completion retirement](../../../docs/reference/memory.md#upload-staging-grows-within-one-rendering-submission), the 220 s shortened route with deferred encoding enabled peaked at 4.24 GiB footprint and 216 MiB live staging, without GPU errors. A 520 s replay of the original input timings peaked at 4.23 GiB / 200 MiB, but landed differently and reached Game Over earlier, so it is not an equivalent traversal comparison. These local results do not establish the cause of the reporter's full-device reboot or rule out longer-term cache growth; the requested save/route, full logs and Jetsam/panic reports are still needed for that comparison. - The NPDM declares 24 MB of system resource, so `IsVirtualAddressMemoryEnabled` is true and the game's memory lives in the alias region. `nninitStartup` also calls `SetHeapSize(0x86000000)` (2.1 GB of the pool as its heap; a 2 GB heap region aborts there with 0xd401). What it can map is bounded by the pool less the heap (1.1 GB). - **Resolved (issue #70):** the resource heaps are address regions in the alias region (2.375 GB here, 64 GB on the Switch). With the SDK's ~1 GB node area at its start, the alias region was used up to +2.25 GB at the first descent from the Great Sky Island with 1.00 GB mapped. The game then took a memory shortage: its frame check (`main+0x1fd32ec`) logs `MSF`, then `R` and `Rf` (reason bits of one resource heap's object), and plays the memory-shortage Blood Moon, which puts Link back at the start. With the node area cut to 62 MB (as Fire Emblem Engage's, Hollow Knight: Silksong's and other builds have it) the high-water mark is +1.75 GB, and no shortage came in 3.7 minutes on the island (it came within a minute in 7 of 8 runs without it). @@ -62,12 +63,14 @@ Game-specific findings. The general host changes they led to are listed under [F ## Mac routes - The issue #70 save: `DM_KEYS="20:A*6/4,f2480:LUp~5"` (title, load, jump, steer into the pond). +- The issue #147 short streaming route uses a fresh copy of the issue #70 save and its corrected SDK module: `DM_FPS_TARGET=30 DM_KEYS="20:A*6/4,f2480:LUp~9,f4000:LUp~20,f4700:LRight~5,f5000:LUp~20"`, run for at least 220 s. This is a fall/respawn stress route, ending at Game Over in both short comparison runs; it is not a long surviving traversal test. The movement timings are repeatable, but the upload burst varies with streaming and CPU/GPU timing, so the deterministic staging regression complements it. - The in-cave autosave (`run/save`): keys `f2500:A,f2800:A,f3100:A,f3600:A`, shot at frame 4500. `DM_DETCLOCK` cannot be used: the game stops at frame 1 waiting for guest time. ## Found on this game General host changes first needed here; each applies to every game: +- **Bounded upload staging and retirement on upload completion:** [Upload staging grows within one rendering submission](../../../docs/reference/memory.md#upload-staging-grows-within-one-rendering-submission). - **Conditional rendering** evaluated at replay (`OP_RENDER_COND`, profile key `render_cond`) and **CPU-written textures with no flush** (`tex_dirty_upload`, `ft_add`): [Conditional rendering evaluated at replay](../../../docs/reference/rendering.md#conditional-rendering-evaluated-at-replay), [CPU writes with no flush, caught by page protection](../../../docs/reference/nvn-objects.md#cpu-writes-with-no-flush-caught-by-page-protection-tex_dirty_upload). - **Mid-pass, partial-mask and partial-scissor clears** drawn as a triangle (`clear_color_tri` / `clear_ds_tri`), **8-bit indices widened on the GPU** when written in the same submission (`wide_run`), **`ReadTexels` from guest memory on any thread** with `nvn_ft_poll` marking textures written after first use, **attachment-less passes** (`bare_dims`): same section. - **Rewriter:** out-of-line atomic retry blocks (`retry_block` / `site_retry_window`, 8 sites) and jump tables relative to an `adr` (`jump_adr_offsets`, `main+0x393bb0`): [Out-of-line retry blocks of atomic updates](../../../docs/reference/rewriter.md#out-of-line-retry-blocks-of-atomic-updates), [Offset tables relative to an adr](../../../docs/reference/rewriter.md#offset-tables-relative-to-an-adr). diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index 70c1056..02d34b8 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -1644,6 +1644,11 @@ static NSMutableArray *g_up_texs; // Target textures in the batch: before the static id g_up_block; static size_t g_up_block_off, g_up_block_size; static NSMutableArray *g_up_keep; +// Limit overflow staging per batch. The three upload slots bound in-flight +// batches; up_begin publishes queued commits before waiting for a slot. A single +// texture larger than the budget gets its own block (uploads are not tiled here). +static size_t g_up_staging_bytes; +#define UP_BATCH_BYTES (128u * 1024u * 1024u) // The upload command buffer was discarded by the system (dropped along during GPU error recovery; the first frame on entering Travincal drops one every run): the objects of this batch of textures are built and the timestamps updated, but the content was never written in, @@ -1669,6 +1674,12 @@ static void up_flush(void) { if (b) { m4_end(b); g_up_ce = nil; } id cb = g_up_cb; unsigned slot = (g_up_seq - 1) % 3; + // Only this upload batch uses its overflow staging. Retire it on upload + // completion, even when the main rendering submission is still being built. + NSMutableArray *staging = g_up_keep; + if (g_up_block && g_up_block != g_up_slot_blk[slot]) [staging addObject:g_up_block]; + g_up_keep = nil; g_up_block = nil; + g_up_block_off = g_up_block_size = g_up_staging_bytes = 0; NSMutableArray *dsts = [NSMutableArray arrayWithCapacity:(NSUInteger)g_nup_blits]; for (int i = 0; i < g_nup_blits; i++) if (g_up_blits[i].dst) [dsts addObject:g_up_blits[i].dst]; static int lost_at = -2; static uint64_t batch_n; if (lost_at == -2) { const char *e = getenv("DM_TEST_UP_LOST"); lost_at = e ? atoi(e) : -1; } int fake_lost = dsts.count && lost_at > 0 && (int)++batch_n == lost_at; MTL4CommitOptions *o = [MTL4CommitOptions new]; @@ -1682,6 +1693,11 @@ static void up_flush(void) { } double d = f.GPUEndTime - f.GPUStartTime; if (d > 0) atomic_fetch_add(&g_upload_gpu_ns, (uint64_t)(d * 1e9)); + @autoreleasepool { + for (id obj in staging) res_remove(obj); + [staging removeAllObjects]; + res_commit(); + } atomic_store(&g_up_alloc_busy[slot], 0); atomic_fetch_sub(&g_up_pending, 1); up_wake(); @@ -1692,9 +1708,7 @@ static void up_flush(void) { // Outside a submission nothing else hands the encode thread its current block until the next submission, so a batch flushed on the spot stayed a record and every wait for it // (nvnQueueFinish on any thread, up_begin on another) waited for that submission: hand it over now (from PR #141, which found the nvnQueueFinish half) if (!g_cb) rec_publish(); - g_up_cb = nil; if (g_up_block) { if (g_up_block != g_up_slot_blk[slot]) defer_obj_release(g_up_block); g_up_block = nil; } g_up_block_off = g_up_block_size = 0; // the slot's own block stays (UP_SLOT_BLOCK) - for (id obj in g_up_keep) defer_obj_release(obj); - [g_up_keep removeAllObjects]; + g_up_cb = nil; up_diag(); g_nup_blits = 0; [g_up_texs removeAllObjects]; @@ -1723,7 +1737,9 @@ static void up_begin(void) { // Diagnostic readout (low frequency; printed only when the block switch count changed and ≥ 1 second since the last line, at most 200 lines per game): how many times an upload batch ran out of staging block and switched blocks midway; GPU allocated, residency set object count, deferred-release queue length. // If the old block is not retired on a switch it stays in the residency set and can never be freed, and "GPU allocated" climbs one block at a time with the switch count static uint64_t g_up_blocks_new, g_up_swaps, g_up_swap_bytes; static NSHashTable *g_up_old; // g_up_old: old blocks switched out (weak references); the diagnostic line counts how many are still alive -static void up_diag(void) { +static void up_diag(void) { @autoreleasepool { + // Weak-table enumeration autoreleases the buffers it finds. Do not keep + // completed batches alive until the whole rendering submission ends. // Print a line when the block switch count changes; then one more line 5 seconds later (that batch's commit is long done and deferred retirement drained, so "GPU allocated" is the settled number) static uint64_t said_swaps = ~0ull, last_ns; static int lines, follow; uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC); int chg = g_up_swaps != said_swaps, fol = follow && now - last_ns >= 5000000000ull; if ((!chg && !fol) || lines >= 200) return; @@ -1733,25 +1749,35 @@ static void up_diag(void) { pthread_mutex_lock(&g_deferred_mu); unsigned long ndef = (unsigned long)g_deferred.count; pthread_mutex_unlock(&g_deferred_mu); MLOG("upload staging blocks%s: %llu created in total, swapped within a batch %llu times (%llu MB of old blocks swapped out); swapped-out blocks still alive now %lu, %llu MB, still in the residency set %lu; GPU allocated %llu MB, residency set %lu objects, deferred-release queue %lu", fol && !chg ? " (settled)" : "", (unsigned long long)g_up_blocks_new, (unsigned long long)g_up_swaps, (unsigned long long)(g_up_swap_bytes >> 20), alive, (unsigned long long)(abytes >> 20), inres, (unsigned long long)(g_dev.currentAllocatedSize >> 20), nres, ndef); -} +} } static id up_alloc(size_t n, size_t *off) { size_t need = (n + 255u) & ~((size_t)255u); + size_t extra = MAX(need, UP_BLOCK_SIZE); + // This call starts one texture's upload: all prior deswizzles and copies are + // complete records, so the batch can be split without ending the render pass. + if (g_up_staging_bytes && (!g_up_block || need > g_up_block_size - g_up_block_off) + && extra > UP_BATCH_BYTES - MIN(g_up_staging_bytes, UP_BATCH_BYTES)) { + up_flush(); + up_begin(); + } if (!g_up_block && need <= UP_BLOCK_SIZE) { // first allocation of the batch: the slot's own block, grown to the next power of two that holds it (a movie frame re-uploaded every frame, 8 MB in Link's Awakening) __strong id *sb = &g_up_slot_blk[g_up_cur_slot]; size_t want = UP_SLOT_BLOCK; while (want < need) want <<= 1; if (*sb && (*sb).length < want) { defer_obj_release(*sb); *sb = nil; } // the slot's previous batch has finished (up_begin waited for it) if (!*sb) { *sb = [g_dev newBufferWithLength:want options:MTLResourceStorageModePrivate]; if (!*sb) MDIE("failed to create the swizzled-upload staging block %#zx", want); res_add(*sb); g_up_blocks_new++; } g_up_block = *sb; g_up_block_size = (*sb).length; g_up_block_off = 0; + g_up_staging_bytes += (*sb).length; } if (!g_up_block || g_up_block_off + need > g_up_block_size) { // Block switch: the old block is still referenced by copy / decompress commands already recorded in this batch and cannot be freed on the spot; hand it to g_up_keep, to be retired later with the other staging after the flush commits (removed from the residency set on retire). // Previously g_up_block was simply replaced with the new block here: nobody retired the old block, yet it stayed in the residency set, so each extra block switch in an upload batch leaked one block (usually 32 MB) of GPU memory. // The old block and the "current block" retired at flush are two objects, so nothing is retired twice - g_up_blocks_new++; if (g_up_block) { g_up_swaps++; g_up_swap_bytes += g_up_block_size; if (!g_up_old) g_up_old = [NSHashTable weakObjectsHashTable]; [g_up_old addObject:g_up_block]; static int leak = -1; if (leak < 0) leak = getenv("DM_TEST_UP_LEAK") != NULL; // test switch DM_TEST_UP_LEAK=1: do not retire the old block, as before the fix (for comparison, to measure the leak) + g_up_blocks_new++; if (g_up_block) { g_up_swaps++; g_up_swap_bytes += g_up_block_size; @autoreleasepool { if (!g_up_old) g_up_old = [NSHashTable weakObjectsHashTable]; [g_up_old addObject:g_up_block]; } static int leak = -1; if (leak < 0) leak = getenv("DM_TEST_UP_LEAK") != NULL; // test switch DM_TEST_UP_LEAK=1: do not retire the old block, as before the fix (for comparison, to measure the leak) if (!leak && g_up_block != g_up_slot_blk[g_up_cur_slot]) { if (!g_up_keep) g_up_keep = [NSMutableArray array]; [g_up_keep addObject:g_up_block]; } } // the slot's block is not retired: it stays for the slot's later batches g_up_block_size = n > UP_BLOCK_SIZE ? need : UP_BLOCK_SIZE; g_up_block = [g_dev newBufferWithLength:g_up_block_size options:MTLResourceStorageModePrivate]; if (!g_up_block) MDIE("failed to create the swizzled-upload staging block %#zx", g_up_block_size); + g_up_staging_bytes += g_up_block_size; res_add(g_up_block); g_up_block_off = 0; } diff --git a/dm/gfx/tests/.gitignore b/dm/gfx/tests/.gitignore index 7b3bc97..c0426dc 100644 --- a/dm/gfx/tests/.gitignore +++ b/dm/gfx/tests/.gitignore @@ -58,6 +58,8 @@ t_tbuf2d t_tbuf2d.dSYM/ t_view_levels t_view_levels.dSYM/ +t_upload_budget +t_upload_budget.dSYM/ t_view_swizzle t_view_swizzle.dSYM/ t_vk_desc diff --git a/dm/gfx/tests/Makefile b/dm/gfx/tests/Makefile index 4478d76..cee36ae 100644 --- a/dm/gfx/tests/Makefile +++ b/dm/gfx/tests/Makefile @@ -108,6 +108,13 @@ t_view_swizzle: t_view_swizzle.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_view_swizzle.m $(MTL_T_SRC) $(FW) t_view_levels: t_view_levels.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_view_levels.m $(MTL_T_SRC) $(FW) +t_upload_budget: t_upload_budget.m $(MTL_T_DEP) + clang $(CFLAGS) -fobjc-arc -o $@ t_upload_budget.m $(MTL_T_SRC) $(FW) +check_t_upload_budget: t_upload_budget + MTL_DEBUG_LAYER=1 ./t_upload_budget + DM_NVN_ENC_THREAD=1 MTL_DEBUG_LAYER=1 ./t_upload_budget +run: check_t_upload_budget +.PHONY: check_t_upload_budget t_linear_readback: t_linear_readback.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_linear_readback.m ../../host/crash.c ../../host/hle_bigstack.c t_host_stubs.c $(filter-out ../nvn/nvn_mtl.m,$(GFX)) $(FW) .PHONY: check_t_linear_readback diff --git a/dm/gfx/tests/t_upload_budget.m b/dm/gfx/tests/t_upload_budget.m new file mode 100644 index 0000000..390d6fb --- /dev/null +++ b/dm/gfx/tests/t_upload_budget.m @@ -0,0 +1,69 @@ +// A streaming burst inside one still-open rendering submission must not retain all +// staging until its rendering fence. Exercise the real Metal upload/retirement path. +#include "../nvn/nvn_mtl.m" +uint64_t g_t0_ns; +int dm_guest_as_contains(uintptr_t address) { (void)address; return 0; } +void *front_metal_layer(void) { return NULL; } +module_t *module_of(const void *p) { (void)p; return NULL; } +module_t g_mod[MAX_MOD]; int g_nmod; +void svc_thread_dump(void) {} +uint64_t now_ns(void) { return bp_now(); } +static int checks, fails; +#define CHECK(c, msg) do { checks++; if (!(c)) { fails++; fprintf(stderr, "FAIL: %s\n", msg); } } while (0) + +static size_t live_bytes(NSHashTable *buffers) { @autoreleasepool { + size_t n = 0; + for (id b in buffers.allObjects) n += b.length; + return n; +} } +static void upload_one(id dst, NSHashTable *buffers, uint8_t value) { + up_begin(); + size_t off; + id b = up_alloc(32u << 20, &off); + @autoreleasepool { [buffers addObject:b]; } + [g_up_ce fillBuffer:b range:NSMakeRange(off, 256) value:value]; + up_blit_add(b, off, 256, 256, MTLSizeMake(1, 1, 1), dst, 0, 0); + [g_up_texs addObject:dst]; +} +int main(void) { @autoreleasepool { + mtl_init(); + NSHashTable *buffers = [NSHashTable weakObjectsHashTable]; + NSMutableArray *textures = [NSMutableArray new]; + MTLTextureDescriptor *d = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:1 height:1 mipmapped:NO]; + d.storageMode = MTLStorageModeShared; + d.usage = MTLTextureUsageRenderTarget; + id target = [g_dev newTextureWithDescriptor:d]; res_add(target); + mtl_sub_begin(); + MTL4RenderPassDescriptor *pass = [MTL4RenderPassDescriptor new]; + pass.colorAttachments[0].texture = target; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + pass.colorAttachments[0].clearColor = MTLClearColorMake(1, 0, 0, 1); + id encoder = [g_cb renderCommandEncoderWithDescriptor:pass]; + size_t peak = 0; + for (int i = 0; i < 64; i++) { + id t = [g_dev newTextureWithDescriptor:d]; + [textures addObject:t]; res_add(t); + upload_one(t, buffers, (uint8_t)(i + 1)); + peak = MAX(peak, live_bytes(buffers)); + } + up_flush(); rec_publish(); up_wait_zero(&g_up_pending, WS_DIAG); + size_t idle = live_bytes(buffers); + CHECK(peak <= (512u << 20), "streaming burst staging stays within 512 MiB"); + CHECK(idle <= (96u << 20), "only three reusable staging blocks survive upload completion before the rendering fence"); + m4_end(encoder); + uint64_t fence = mtl_sub_commit(); + CHECK(mtl_wait(fence, 5000000000ull), "rendering submission completes"); + CHECK(!mtl_last_commit_error(), "no rendering GPU error"); + uint8_t clear[4] = {0}; + [target getBytes:clear bytesPerRow:4 fromRegion:MTLRegionMake2D(0, 0, 1, 1) mipmapLevel:0]; + CHECK(clear[0] == 255 && clear[1] == 0 && clear[2] == 0 && clear[3] == 255, + "the open rendering pass survives upload commits"); + for (int i = 0; i < 64; i++) { + uint8_t pixel[4] = {0}; + [textures[i] getBytes:pixel bytesPerRow:4 fromRegion:MTLRegionMake2D(0, 0, 1, 1) mipmapLevel:0]; + for (int c = 0; c < 4; c++) CHECK(pixel[c] == i + 1, "upload pixels survive batch splitting and staging retirement"); + } + printf("upload budget: peak %zu MiB, completed %zu MiB, %d checks, %d failed\n", peak >> 20, idle >> 20, checks, fails); + return fails != 0; +} } diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index e145139..5fdcf5b 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -61,6 +61,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - the frames stop at the first loading screen, a graphics worker at 100% CPU (17 s in, `UnityGfxDeviceWorker` running, the main thread waiting on a condition variable, no red line). → [Unity's GPU pool block allocation spins at size 0](../reference/memory.md#unitys-gpu-pool-block-allocation-spins-at-size-0) - SIGBUS inside a host hook (`snprintf` under `mount_ex` / `h_fs_MountSave`), then the process hangs (Super Mario 3D World + Bowser's Fury); SIGBUS inside Metal (`AGCDeserializedReply`, an AGX compiler block) at an address in the guest stack region (Breath of the Wild). → [Host calls on small guest stacks](../reference/memory.md#host-calls-on-small-guest-stacks) - the iPhone app is killed for memory; resident memory grows steadily (about 0.2 MB a frame) while the residency set and the texture counts stay flat. → [Deferred-release drain without an autorelease pool](../reference/memory.md#deferred-release-drain-without-an-autorelease-pool) +- travelling produces a brief multi-GB footprint spike, then memory returns to its earlier level; periodic memory lines can miss the peak. Upload diagnostics show many swapped-out blocks still alive in one batch. → [Upload staging grows within one rendering submission](../reference/memory.md#upload-staging-grows-within-one-rendering-submission) ## Threads diff --git a/docs/reference/memory.md b/docs/reference/memory.md index 0c0078c..2a0dca4 100644 --- a/docs/reference/memory.md +++ b/docs/reference/memory.md @@ -270,3 +270,11 @@ The guest address space, heaps, pools, virtual address memory and the allocators - **Fix:** `defer_drain` has its own `@autoreleasepool`. - **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md): GitHub issue #9 (iPhone 16 Pro Max, handheld), killed in the first shrine. An 8 GB iPhone gives this app about 3.4 GB (resident plus `headroom to the system memory limit` stayed at 3,375 MB); resident memory grew from 2.6 GB at the shrine load until the kill. On the Mac a host without that pool grew 1 MB a frame in the shrine (GPU 3.0 → 4.4 GB in 2,400 frames); with it the shrine stays at 2.7–2.9 GB resident and 1.5–1.7 GB GPU, idle or walking (vmmap at 2.8 GB: about 1.6 GB guest memory, 0.75 GB private textures, 0.17 GB IOAccelerator). - **Note:** found on the way: dead texture serials and the fixed-size pool tracking table ([Dead texture serials kept in a ghost table](nvn-objects.md#dead-texture-serials-kept-in-a-ghost-table), [Memory-pool tracking table grows in blocks](nvn-objects.md#memory-pool-tracking-table-grows-in-blocks)). + +### Upload staging grows within one rendering submission + +- **Symptom:** travelling produces a brief multi-GB footprint spike, then memory returns to its earlier level; periodic memory lines can miss the peak. Upload diagnostics show many swapped-out blocks still alive in one batch. +- **Cause:** `up_alloc` could append unlimited staging blocks before `up_flush`. Overflow staging then retired on the main rendering fence, so splitting uploads alone still retained completed staging while that submission was being built. Weak-table insertion and enumeration in the staging diagnostics also left temporary strong references in the submission's autorelease pool. +- **Fix:** split GPU deswizzle batches before another block would exceed 128 MiB. The three upload allocator slots provide backpressure, publishing deferred commits before waiting. Retire overflow and CPU-upload staging from the upload completion handler, remove residency and clear its retained array before freeing the slot; keep diagnostic weak-table operations in their own autorelease pools. Rendering destinations keep their existing lifetime. A single texture larger than the batch budget still gets its own block; this is not a cap on all renderer memory. +- **Check:** `make -C dm/gfx/tests GAME= check_t_upload_budget` uses actual Metal uploads during an open render pass, with direct and deferred encoding and Metal validation. A 2 GiB burst retained 2,048 MiB before the fix; afterwards it peaks at 384 MiB and retains only the three reusable blocks (96 MiB) after upload completion, before the rendering fence. It also checks every uploaded pixel and the render pass's clear. For a game, capture both frequent `phys_footprint` samples and the kernel's peak ledger; the existing `resident` log label denotes footprint, and staging blocks `created in total` is cumulative, not live memory. +- **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md#memory), investigating issue #147. Mac reproduction and device validation are tracked in the game note. From caee62a853ade4a136cd51bf2eeeab1c60df211d Mon Sep 17 00:00:00 2001 From: seedds Date: Mon, 26 Oct 2026 01:20:58 +0800 Subject: [PATCH 3/6] Compact NVN buffer bindings and remove Wonder heap workaround --- dm/games/010015100B514000/README.md | 20 +++--- dm/games/010015100B514000/game.tsv | 2 +- dm/games/010015100B514000/patches.py | 37 ---------- dm/gfx/nvn/hle_nvn.c | 8 ++- dm/gfx/nvn/nvn_mtl.m | 12 ++-- dm/gfx/nvn/nvn_obj.h | 17 ++++- dm/gfx/tests/t_nvn.c | 100 ++++++++++++++++++++++++++- dm/gfx/tests/t_reorder_chain.m | 16 ++++- dm/gfx/tests/t_vbuf.m | 15 +++- docs/agents/symptoms.md | 3 +- docs/reference/memory.md | 18 ++--- docs/reference/rewriter.md | 4 +- 12 files changed, 172 insertions(+), 80 deletions(-) delete mode 100644 dm/games/010015100B514000/patches.py diff --git a/dm/games/010015100B514000/README.md b/dm/games/010015100B514000/README.md index 2c14dfc..f2f86ae 100644 --- a/dm/games/010015100B514000/README.md +++ b/dm/games/010015100B514000/README.md @@ -6,30 +6,31 @@ Base game v0 (1.0.0, SDK 16.2.0), update 1.0.1 (v65536, SDK 16.2.0; short Mac cr | Setting | Where | Why | |---|---|---| -| Resource heap +16 KB | [`patches.py`](patches.py) `CUTS`, v0 `main` `cd6e42ae…` at `main+0x1e62fc`, 1.0.1 `main` `f91868b8…` at `main+0x1f6890`, 1.2.1 `main` `ff773e90…` at `main+0x1cf9b0` | a resource heap came up about 0x70 bytes short; see [Memory](#memory) | | 5 GB guest layout: `mem_reserve 0x140000000`, `mem_alias 0xB8000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000` | [`game.tsv`](game.tsv) | 256 MB in front + 2.875 GB alias region + 256 MB stack region + 1.625 GB heap region: the game maps 1.75 GB of virtual address memory on demand into the alias region and asks for a 1.47 GB heap | | `shader_versions v0 v327680` | `game.tsv` | the shader tables are the union of both versions, so packing either needs both NSPs | -No `dm/tools/sdk_patches.py` cut for its `sdk` builds (`3053b229…` v0, `e4bd58ca…` 1.2.1). No `render_cond`: a minute's boot counted 65,536 or more clears under render conditions, and the game needs none. No `pad_layout`: the virtual gamepad starts in the 2D layout. +No game-specific executable cuts. No `dm/tools/sdk_patches.py` cut for its `sdk` builds (`3053b229…` v0, `e4bd58ca…` 1.2.1). No `render_cond`: a minute's boot counted 65,536 or more clears under render conditions, and the game needs none. No `pad_layout`: the virtual gamepad starts in the 2D layout. ## Versions - v0 (1.0.0), 1.0.1 (v65536) and 1.2.1 (v327680). With 1.2.1 layered over v0, unchanged files become APFS clones, so the merged RomFS adds only about 114 MB to the 3.5 GB base. -- 1.2.1 is a new `main` build (SDK 16.2.12). Its resource-heap cut was carried over from v0 with `dm/tools/nso_fnmap.py` (containing function `0x1e603c` → `0x1cf6f0`, identical after address masking; `ExpHeap::create` `0x4aac38` → `0x4c4a18`). - 1.2.1 imports 28 SDK functions v0 did not, which the host answers: `nn::nfp` (amiibo, initialized at boot) finds none, `pl::IsRunningOnOunce` (is this a Switch 2) is false, `ec::ShowShopApplicationInformationForUpgrade` is a no-op, `account::PopOpenUsers` returns none and `aoc::CountAddOnContent` none. Mario only reads the count from `PopOpenUsers` and falls back to `TryOpenPreselectedUser` on 0, and counts add-on content only on a Switch 2 (`hle_sdk16.c`, `hle_account.c`, `hle_misc.c`). - 1.2.1 compresses the NACP's title area (a u16 size, then raw deflate inflating to 32 entries of 0x300 bytes), which LibHac 0.19 does not decode; `nx_extract` and the NSP script do. Its patch NCA (key generation 17) makes hactool stop with `Invalid BKTR layout!`, which is why `nx_extract` uses LibHac. ## Memory -- **Current: function matching on unregistered main builds.** `MATCH_CUTS` in `patches.py` recognizes the complete 1,420-byte resource-heap setup function and its direct callees/imports, then derives the cut at function +0x2c0. The v0 and 1.2.1 code require separate fingerprints: a directly called helper changed from 72 to 76 bytes. Both fingerprints locate exactly the corresponding verified cut. A full rewrite of 1.2.1 with a synthetic unregistered build ID produced a byte-identical `main.patched.bin` to its known-build rewrite. This verifies patch selection and rewriting; it is not a new gameplay or iPhone test. Known builds keep their fixed cuts; an unfamiliar or ambiguous implementation is reported and skipped. This covers compatible code in later builds, not arbitrary update compatibility. See [Function-matched cuts](../../../docs/reference/rewriter.md#function-matched-cuts-for-unregistered-builds) for the shared mechanism and its limits. +- **Resolved: resource heaps exhausted by oversized host command records.** The game measures scratch recordings while building model resources (`main+0x57500`, shape/pass recording `+0x5b8c0`, mesh draw measurement `+0x593ec`, 1.2.1). Those measurements feed the GPU chunk size, although the resource heap itself is fixed by Nintendo's resource-size table. The measured templates contain uniform and vertex-buffer bindings whose host records were larger than the native commands. Compacting these shared host records fixes the allocation. See [Model heaps sized by command records](../../../docs/reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes). -- **Resolved for the tested 1.0.1 startup path: null memory pool after resource allocation.** The unpatched build (`main` `f91868b88f60d3d59009db3389fde314`, `sdk` `3053b22935e32ddf15ef81997603e5c2`) logged `BufferInitialize: pool has no storage (pool 0x0 ... size 0x280)` and crashed with `sig=11 addr=0x4 pc=h_nvnMemoryPoolGetFlags+0`. Its heap-size setup function is the unique full 1,420-byte address-masked match of v0's `0x1e603c`, at `0x1f65d0`; both calls of v0's `ExpHeap::create` (`0x4aac38`) map to `0x48b134`, whose full masked function also matches. The existing +16 KB cut therefore maps from `0x1e62fc` to `0x1f6890`, with the same asserted original word. On an Apple M5 Pro Mac, isolated saves and module copies reproduced the unpatched crash after 10.99 s (last reported frame 471); changing only this word ran for 35.74 s (last reported frame 1827), with no null-pool diagnostic or crash, before controlled termination. These are short headless correctness runs, not full gameplay, save, iPhone or performance acceptance. When only v0 and 1.0.1 are supplied, the script still derives this version's own shader tables because the checked-in union also needs 1.2.1. +- **Measured failure and fix (1.2.1).** With the old heap allowance disabled, `Model/PlayerMarioSuper.bfres` receives table value `0x80b660` + `0x1008`, a `0x80c668`-byte ExpHeap. Its final MatBlock chunk was `0xc000` bytes, leaving `0x648` free for a `0x6b8` descriptor: 112 bytes short. Payloads, block headers and alignment padding account for the entire heap, so this was not free-list corruption. Compact uniform bindings alone reduce the requested GPU budget from `0xb8d4` to `0xb76c` and still fail; compact single vertex bindings reduce it to `0xa6ec`, so the chunk becomes `0xb000` and the descriptor fits. Compact render-state records had no effect because these scratch templates do not contain them; that experiment was reverted. + +- **Validation and remaining scope.** On the Mac, fresh isolated saves and `DM_KEYS='5:A,7:A'` reproduce the unpatched 1.2.1 null-pool crash in about 8 seconds. The general fix passed that route, then 60-second headless runs of both 1.0.0 and 1.2.1 with additional A presses, each reaching reported frame 3300 without the heap allowance, null-pool diagnostic or crash. `t_nvn` passes 152 checks; `t_reorder_chain` passes 171 including compact/full uniform-binding pixel equivalence; `t_vbuf` passes 45 including compact vertex bindings and GPU readback. These game runs use no translated shader libraries and establish CPU allocation progress, not full rendering, gameplay or iPhone acceptance. A fresh 1.2.1 rewrite after removing the profile patch emits zero cuts and is byte-identical to the executable used in those successful runs. 1.0.1 was not rerun with this fix. + +- **Removed workaround.** The former +16 KB per-resource cuts at v0 `main+0x1e62fc`, 1.0.1 `+0x1f6890` and 1.2.1 `+0x1cf9b0`, and their `MATCH_CUTS` rules, are removed. The original v0 failure was the same null-pool symptom for `Model/NpcCommonerA.bfres` after its `0x9000` MatBlock chunk. The shared [function matcher](../../../docs/reference/rewriter.md#function-matched-cuts-for-unregistered-builds), originally verified using this workaround's 1,420-byte heap-setup function across v0 and 1.2.1, remains available to other profiles; Wonder no longer enrolls a rule. - The game reserves "virtual address memory" (1.75 GB in `System/HeapSize/*.HeapDef.byml`) and maps it into the alias region with `MapPhysicalMemory` on demand. With the alias region left after the node area too small, large resources were silently skipped and a null deref followed where they were used, also visible as `SetHeapSize` asking for much less than the pool. The 5 GB layout above fixed it. - Its two modules are about 74 MB, and its heap request matches hardware with the host's 64 MB of "used" memory. -- **Current: every resource heap gets +16 KB.** Each resource gets its own `sead::ExpHeap` sized resource-size-table value + 0x1008 (`main+0x1e603c` → `ExpHeap::create` `main+0x4aac38`, v0). The table values are Nintendo's measured peaks with almost no margin (Yoshi's animation file `PlayerYoshiAnm` uses 99% of its budget). `Model/NpcCommonerA.bfres` in the opening course (Course900) came up about 0x70 bytes short: its last GPU chunk (MatBlock, 0x9000 bytes, 4 KB aligned, `main+0x4290c0`) fit, but the chunk's 0x598-byte descriptor did not; a buffer was then built on a null memory pool and the game crashed on a null read at `main+0x106f5c8`. The cut makes `mov x0,x23` → `add x0,x23,#0x4000` (the `ExpHeap::create` size argument). - The single 128 MB NVN virtual memory pool (flag 0x200) is mapped onto one physical pool in one request, so the host aliases it (`hle_nvn.c`). Before, a failed `MapVirtual` gave a buffer with a null pool right after a scene change. -- How the heap was traced: probes on the gsys GPU allocator (`main+0x4290c0`) and the heap's `tryAlloc` (vtable slot +0x30); the `!xN` heap walk uses Mario's sead layout by default (`DM_PROBE_HEAP` `0,28,30,b0,c8`; [05, Probes](../../../docs/agents/05-debugging.md#probes-without-a-debugger-dm_probe)). +- How the heap was traced: probes on the gsys GPU allocator (`main+0x4290c0` in v0, `+0x4286e0` in 1.2.1) and the heap's `tryAlloc` (vtable slot +0x30); the `!xN` heap walk uses Mario's sead layout by default (`DM_PROBE_HEAP` `20,28,30,b0,c8`; [05, Probes](../../../docs/agents/05-debugging.md#probes-without-a-debugger-dm_probe)). ## Frame rate @@ -94,11 +95,11 @@ Game-specific findings; the general host changes they led to are listed under [F General host changes first needed here; each applies to every game: -- **Function matching for opt-in cuts across updates**: [Function-matched cuts](../../../docs/reference/rewriter.md#function-matched-cuts-for-unregistered-builds). Shared rewriter support; Wonder is the first enrolled profile. +- **Function matching for opt-in cuts across updates**: [Function-matched cuts](../../../docs/reference/rewriter.md#function-matched-cuts-for-unregistered-builds). Shared rewriter support first verified on Wonder; its heap workaround and enrollment have since been removed. - **NVN on Metal basics** in [rendering.md](../../../docs/reference/rendering.md): [8-bit index widening](../../../docs/reference/rendering.md#8-bit-index-buffers), [texture swizzles](../../../docs/reference/rendering.md#texture-swizzle-through-swizzled-metal-views) (`nvn_fmt_to_mtl`, `img_fit`), [format-changing render-target views](../../../docs/reference/rendering.md#render-target-views-that-change-the-format) (`attach`, `present_raw_src`), [storage-shared transient targets](../../../docs/reference/rendering.md#transient-render-targets-shared-by-storage), [inline uniform updates as in-order copies](../../../docs/reference/rendering.md#inline-uniform-updates-as-ordered-gpu-copies) snapshotted at encode (`mtl_sub_update`), [CPU-written linear textures](../../../docs/reference/nvn-objects.md#linear-textures-in-cpu-visible-pools) on the pool buffer (`tex_linear`), [integer vertex inputs](../../../docs/reference/rendering.md#integer-vertex-inputs) (`int_inputs`, `vfmt_for_ct`), [intra-encoder barriers](../../../docs/reference/rendering.md#metal-4-does-not-order-operations-inside-one-encoder) (`blit_buf`, `DM_NVN_BLIT_ORDER`), [zero-filled new textures](../../../docs/reference/rendering.md#new-textures-start-as-zero) (`tex_zero_fill`), [scissored zero clears](../../../docs/reference/rendering.md#scissored-colour-clears) (`clear_rect_zero`), [vertex output padding from shader pairs](../../../docs/reference/shaders.md#pixel-shader-inputs-the-vertex-shader-does-not-write) (`vs_pad`, `shader_pairs.tsv`), [virtual pools aliased onto one physical pool](../../../docs/reference/nvn-objects.md#virtual-memory-pools-mapped-onto-one-physical-pool). - **Binding layout and handles in uniform buffers** (`NVN_RB_TEXU` / `NVN_RB_SAMPU`): [04, section 3](../../../docs/agents/04-graphics.md#3-find-the-binding-layout-by-census); the compute code start: [Compute code with and without a header](../../../docs/reference/shaders.md#compute-code-with-and-without-a-header). - **The SDK's audio renderer in-process** and the catch-up frame between two updates: [Running the SDK's own renderer on the CPU](../../../docs/reference/audio.md#running-the-sdks-own-renderer-on-the-cpu), [At least one rendered frame between two updates](../../../docs/reference/audio.md#at-least-one-rendered-frame-between-two-updates). -- **Resource heaps cut by build ID** (`CUTS` in `patches.py`), **memory partition** (`mem_init`), **virtual NVN pools**: [Per-resource heaps sized from Nintendo's measured peaks](../../../docs/reference/memory.md#per-resource-heaps-sized-from-nintendos-measured-peaks), [Partitioning the guest block between alias region, stacks and heap](../../../docs/reference/memory.md#partitioning-the-guest-block-between-alias-region-stacks-and-heap), [Virtual memory pools mapped onto one physical pool](../../../docs/reference/nvn-objects.md#virtual-memory-pools-mapped-onto-one-physical-pool). +- **Compact buffer bindings keep model heaps within their budgets**, **memory partition** (`mem_init`), **virtual NVN pools**: [Model heaps sized by command records](../../../docs/reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes), [Partitioning the guest block between alias region, stacks and heap](../../../docs/reference/memory.md#partitioning-the-guest-block-between-alias-region-stacks-and-heap), [Virtual memory pools mapped onto one physical pool](../../../docs/reference/nvn-objects.md#virtual-memory-pools-mapped-onto-one-physical-pool). - **Update imports diffed before the first run**, **fixed-address code gated by `module_t.build_id`**, **BFTTF shared font** (`hle_sdk16.c`), **the extension font**, **`GetCommandMemoryUsed` counting host segments**, **`NVNcommandBuffer` +0x10** (`nvn_flag10`), **`prog_t` size**: [A game update imports SDK calls the base game did not](../../../docs/reference/services.md#a-game-update-imports-sdk-calls-the-base-game-did-not), [Code inherited from another title at fixed addresses](../../../docs/reference/services.md#code-inherited-from-another-title-at-fixed-addresses), [Shared font address: BFTTF header or bare TTF](../../../docs/reference/services.md#shared-font-address-bfttf-header-or-bare-ttf), [Nintendo-extension glyphs for button icons](../../../docs/reference/services.md#nintendo-extension-glyphs-for-button-icons), [What `GetCommandMemoryUsed` counts](../../../docs/reference/nvn-objects.md#what-getcommandmemoryused-counts), [Inlined SDK code writes into `NVNcommandBuffer` +0x10](../../../docs/reference/nvn-objects.md#inlined-sdk-code-writes-into-nvncommandbuffer-0x10), [Host records must fit the game's SDK object sizes](../../../docs/reference/nvn-objects.md#host-records-must-fit-the-games-sdk-object-sizes). - **Exclusive windows** (fall-through exit first, Mario had 82): [Exit stub order in exclusive windows](../../../docs/reference/rewriter.md#exit-stub-order-in-exclusive-windows). **Ideal core −2**: [Ideal core −2 resolves to the process default core](../../../docs/reference/threads.md#ideal-core-2-resolves-to-the-process-default-core). - **Present-interval holds** (`g_link_hold_until`, `nvn_game_fps`): [Frames of a dynamic present interval held on screen and at the acquire](../../../docs/reference/frame-rate.md#frames-of-a-dynamic-present-interval-held-on-screen-and-at-the-acquire). @@ -107,7 +108,6 @@ General host changes first needed here; each applies to every game: ## Open - On the Mac, Mario's menu and dialogue text render blank (fine on the iPhone), per development.md's known gaps. -- Why the resource heap is short is not known (`patches.py`: every allocation size before that block is set by game code and data); Breath of the Wild's identical failure came from host record sizes, and whether Mario's chunk is sized the same way is not checked yet. - dm/games/README.md says the heap check turns "Wonder's 1.44 GB" request into a 0xc0000000 alias region; 03 says it asks for 1.47 GB, and the profile's alias region is 0xB8000000. - Several addresses in 02 and 03 (`main+0x1070910`, `main+0x107dbc0`, `main+0x4da5c`, `main+0xb8dec`, `main+0xcfd24`, `main+0x36ceff0`) do not name their build; the present-interval ones are from 1.2.1. - 2 agl post-effect compute shaders do not translate. diff --git a/dm/games/010015100B514000/game.tsv b/dm/games/010015100B514000/game.tsv index ba038d3..e133ced 100644 --- a/dm/games/010015100B514000/game.tsv +++ b/dm/games/010015100B514000/game.tsv @@ -1,4 +1,4 @@ -# Game profile of Super Mario Bros. Wonder (Switch, base game v0 and update 1.2.1). This folder also holds the shader tables (generated by shader_split.py), pso_manifest.tsv, patches.py (the rewriter's patches of its builds) and, not committed, c1_full.tsv and cb_extent.tsv +# Game profile of Super Mario Bros. Wonder (Switch, base game v0 and update 1.2.1). This folder also holds the shader tables (generated by shader_split.py), pso_manifest.tsv and, not committed, c1_full.tsv and cb_extent.tsv # Format: key value; several values of one key are separated by spaces; one build row per version; comments must be on their own line. Rebuild the host after changing memory / NVN constants; after changing shader-related keys follow the chain procedure in AGENTS.md # ---- Identity ---- diff --git a/dm/games/010015100B514000/patches.py b/dm/games/010015100B514000/patches.py deleted file mode 100644 index ee738bc..0000000 --- a/dm/games/010015100B514000/patches.py +++ /dev/null @@ -1,37 +0,0 @@ -# Super Mario Bros. Wonder (010015100B514000): the rewriter's patches of this game's own builds. dm/tools/patch_nso.py loads every profile's patches.py and applies those of a module's NSO build ID; -# CUTS, OBS and FRAME_STEP as described there ("Registered patches"). Patches of Nintendo SDK module builds are in dm/tools/sdk_patches.py -CUTS = { - # Super Mario Bros. Wonder v0 main - 'cd6e42aee7934f4d8393cd43893ae25e': [ - # Resource heap slack: each resource unit's ExpHeap is created with the resource size table value + 0x1008 (main+0x1e603c → ExpHeap::create main+0x4aac38); the table holds Nintendo's measured peaks with almost no margin - # (PlayerYoshiAnm uses 99%). On the host, Model/NpcCommonerA.bfres in the opening course Course900 is about 0x70 bytes short: after the model resource's last GPU chunk (MatBlock, - # 0x9000 bytes, 4 KB aligned, main+0x4290c0) is placed, its 0x598-byte descriptor does not fit, the GPU allocation fails → a buffer is built on a null memory pool → null read crash at main+0x106f5c8. - # Where this differs from hardware is not known (every allocation size before this block is determined by game code and data); for now every resource heap gets +16 KB. - (0x1e62fc, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 → add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)'), - ], - # Super Mario Bros. Wonder 1.0.1 (v65536), main f91868b8...: resource heap slack as above. - # Ported from v0 by the unique address-masked match of the complete 1,420-byte function - # 0x1e603c -> 0x1f65d0; both ExpHeap::create call sites also map to the same callee - # (0x4aac38 -> 0x48b134). The original instruction is asserted by the rewriter. - 'f91868b88f60d3d59009db3389fde314': [ - (0x1f6890, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 -> add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)'), - ], - # Super Mario Bros. Wonder 1.2.1 (v327680) main: same as above. Location carried over from v0 by dm/tools/nso_fnmap.py (containing function 0x1e603c → 0x1cf6f0 identical after address masking, ExpHeap::create 0x4aac38 → 0x4c4a18) - 'ff773e90972d544eb79406eaa65396d5': [ - (0x1cf9b0, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 → add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)'), - ], -} - -# Unknown main builds: identify the complete heap setup function and its direct -# callees/imports, not an address or the short mov/add sequence. Verified against -# v0 (0x1e603c) and 1.2.1 (0x1cf6f0); a helper changed between them, so each has its -# own fingerprint. Known builds above keep their exact original-byte assertions. -MATCH_CUTS = {'main': [{ - 'name': 'resource heap slack', - 'size': 1420, - 'fingerprints': [ - '8062801c559542f56d41eda82685f85f0302933458a57fca074e9b3a3a475102', - '09233b021587d058cf645d301d58d07e3dc2bdbac1737b874bb7a0ba914725ed', - ], - 'cuts': [(0x2c0, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 -> add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)')], -}]} diff --git a/dm/gfx/nvn/hle_nvn.c b/dm/gfx/nvn/hle_nvn.c index adf4ac7..208cb05 100644 --- a/dm/gfx/nvn/hle_nvn.c +++ b/dm/gfx/nvn/hle_nvn.c @@ -936,13 +936,19 @@ NVN(nvnCommandBufferBindVertexStreamState, void, NVNcommandBuffer *c, int n, con } NVN(nvnCommandBufferBindVertexBuffers, void, NVNcommandBuffer *c, int first, int n, const NVNbufferRange *rg) { if (n < 0 || first + n > 16) { LOG("[RED] BindVertexBuffers first=%d n=%d", first, n); return; } + if (n == 1 && (unsigned)first < 16 && rg[0].size <= UINT32_MAX) { + *REC(c, OP_BIND_VBUF1, p_bind_vbuf1) = (p_bind_vbuf1){(uint32_t)first, (uint32_t)rg[0].size, rg[0].address}; return; + } p_bind_vbuf *r = RECN(c, OP_BIND_VBUF, p_bind_vbuf, r, n); r->first = first; r->n = n; memcpy(r->r, rg, sizeof *rg * n); } NVN(nvnCommandBufferBindUniformBuffer, void, NVNcommandBuffer *c, NVNshaderStage st, int i, NVNbufferAddress a, size_t n) { + if ((unsigned)st < 6 && (unsigned)i < 16 && n <= UINT32_MAX) { + *REC(c, OP_BIND_UBO_SMALL, p_bind_ubo_small) = (p_bind_ubo_small){(uint16_t)st, (uint16_t)i, (uint32_t)n, a}; return; + } *REC(c, OP_BIND_UBO, p_bind_buf) = (p_bind_buf){st, i, a, n}; } NVN(nvnCommandBufferBindUniformBuffers, void, NVNcommandBuffer *c, NVNshaderStage st, int first, int n, const NVNbufferRange *rg) { - for (int i = 0; i < n; i++) *REC(c, OP_BIND_UBO, p_bind_buf) = (p_bind_buf){st, first + i, rg[i].address, rg[i].size}; + for (int i = 0; i < n; i++) h_nvnCommandBufferBindUniformBuffer(c, st, first + i, rg[i].address, rg[i].size); } NVN(nvnCommandBufferBindStorageBuffer, void, NVNcommandBuffer *c, NVNshaderStage st, int i, NVNbufferAddress a, size_t n) { *REC(c, OP_BIND_SSBO, p_bind_buf) = (p_bind_buf){st, i, a, n}; diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index 02d34b8..fd04b69 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -5495,8 +5495,8 @@ static int state_op(uint32_t op, const void *a) { } case OP_BIND_VATTR: { const p_bind_vattr *b = a; if (S.nattr != b->n || memcmp(S.attr, b->st, 4 * b->n)) { memcpy(S.attr, b->st, 4 * b->n); S.nattr = b->n; g_pso_dirty = 1; } break; } case OP_BIND_VSTREAM: { const p_bind_vstream *b = a; if (S.nstream != b->n || memcmp(S.stream, b->st, 8 * b->n)) { memcpy(S.stream, b->st, 8 * b->n); S.nstream = b->n; g_pso_dirty = 1; } break; } - case OP_BIND_VBUF: { const p_bind_vbuf *b = a; memcpy(&S.vbuf[b->first], b->r, sizeof *b->r * b->n); break; } - case OP_BIND_UBO: { const p_bind_buf *b = a; if (b->stage < 6 && b->index < 16 && memcmp(&S.ubo[b->stage][b->index], b, sizeof *b)) { S.ubo[b->stage][b->index] = *b; g_args_dirty[b->stage] = 1; } break; } + case OP_BIND_VBUF: case OP_BIND_VBUF1: { p_bind_vbuf tmp; const p_bind_vbuf *b = vbuf_rec(op, a, &tmp); memcpy(&S.vbuf[b->first], b->r, sizeof *b->r * b->n); break; } + case OP_BIND_UBO: case OP_BIND_UBO_SMALL: { p_bind_buf tmp; const p_bind_buf *b = bind_buf_rec(op, a, &tmp); if (b->stage < 6 && b->index < 16 && memcmp(&S.ubo[b->stage][b->index], b, sizeof *b)) { S.ubo[b->stage][b->index] = *b; g_args_dirty[b->stage] = 1; } break; } case OP_BIND_SSBO: { const p_bind_buf *b = a; if (b->stage < 6 && b->index < 16 && memcmp(&S.ssbo[b->stage][b->index], b, sizeof *b)) { S.ssbo[b->stage][b->index] = *b; g_args_dirty[b->stage] = 1; } break; } case OP_BIND_TEX: case OP_BIND_TEX1: { p_bind_handles t1; const p_bind_handles *b = bind_rec(op, a, &t1); if (b->n < 0 || b->n > 32) { // the recorder never writes more than 32 (bind_handles): the record was overwritten after it was recorded @@ -7214,8 +7214,8 @@ static void lc_take(NvProg *pr) { static int lc_keeps(uint32_t op) { switch (op) { case OP_SET_RT: case OP_CLEAR_COLOR: case OP_CLEAR_DS: case OP_DRAW: case OP_DRAW_IDX: case OP_BEGIN: case OP_JUMP: case OP_CALL: - case OP_SCISSOR: case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_VBUF: - case OP_BIND_UBO: case OP_BIND_SSBO: case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_SAMP: case OP_BIND_IMG: case OP_STENCIL: + case OP_SCISSOR: case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_VBUF: case OP_BIND_VBUF1: + case OP_BIND_UBO: case OP_BIND_UBO_SMALL: case OP_BIND_SSBO: case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_SAMP: case OP_BIND_IMG: case OP_STENCIL: case OP_POLY_OFFSET: case OP_VIEWPORTS: case OP_DEPTH_RANGE: case OP_DEPTH_CLAMP: case OP_VP_SWIZZLE: case OP_BLEND_COLOR: case OP_COPY_STRIDE: case OP_TF_BIND: case OP_TF_STATE: case OP_RAST_DISCARD: case OP_PRIM_RESTART: case OP_BARRIER: case OP_SET_TEX_POOL: case OP_SET_SAMP_POOL: case OP_RENDER_COND: case OP_UPDATE_UB: case OP_COPY_B2B: case OP_CLEAR_BUF: return 1; @@ -8239,8 +8239,8 @@ void mtl_exec(const uint8_t *p) { case OP_DRAW: { if (g_rc_skip) break; p_draw t; g_exec_cur = p; g_exec_next = p + h.size; draw(draw_rec(a, h.size, &t), NULL); g_exec_next = NULL; break; } // Lookahead start point case OP_DRAW_IDX: { if (g_rc_skip) break; p_draw_idx t; g_exec_cur = p; g_exec_next = p + h.size; draw(NULL, draw_idx_rec(a, h.size, &t)); g_exec_next = NULL; break; } // Do not mark dirty if the state did not change (the game often re-sends the same program / state block / vertex format before each draw; the dirty flag only decides whether to re-fetch the pipeline / depth-stencil object, and equal values mean the same object) - case OP_SCISSOR: case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_VBUF: - case OP_BIND_UBO: case OP_BIND_SSBO: case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_SAMP: case OP_BIND_IMG: case OP_STENCIL: + case OP_SCISSOR: case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_VBUF: case OP_BIND_VBUF1: + case OP_BIND_UBO: case OP_BIND_UBO_SMALL: case OP_BIND_SSBO: case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_SAMP: case OP_BIND_IMG: case OP_STENCIL: case OP_POLY_OFFSET: case OP_VIEWPORTS: case OP_DEPTH_RANGE: case OP_DEPTH_CLAMP: case OP_VP_SWIZZLE: case OP_BLEND_COLOR: case OP_COPY_STRIDE: state_op(h.op, a); break; case OP_TF_BIND: { const p_tf_bind *b = a; PD("Y tf bind %d addr=%#llx size=%llu", b->index, (unsigned long long)b->addr, (unsigned long long)b->size); state_op(h.op, a); break; } diff --git a/dm/gfx/nvn/nvn_obj.h b/dm/gfx/nvn/nvn_obj.h index e2af98a..922dbc9 100644 --- a/dm/gfx/nvn/nvn_obj.h +++ b/dm/gfx/nvn/nvn_obj.h @@ -103,7 +103,7 @@ enum { OP_BIND_PROGRAM, OP_BIND_STATE, OP_BIND_VATTR, OP_BIND_VSTREAM, OP_BIND_VBUF, OP_BIND_UBO, OP_BIND_SSBO, OP_BIND_TEX, OP_BIND_SAMP, OP_STENCIL, OP_POLY_OFFSET, OP_DRAW, OP_DRAW_IDX, OP_DISPATCH, OP_BARRIER, - OP_SET_TEX_POOL, OP_SET_SAMP_POOL, OP_COPY_B2T, OP_COPY_STRIDE, OP_BLEND_COLOR, OP_COPY_T2B, OP_BIND_IMG, OP_COPY_T2T, OP_RESET_COUNTER, OP_CLEAR_TEX, OP_CLEAR_BUF, OP_DEPTH_CLAMP, OP_RENDER_COND, OP_FENCE_SYNC, OP_TF_BIND, OP_TF_STATE, OP_RAST_DISCARD, OP_VP_SWIZZLE, OP_BIND_TEX1, OP_PRIM_RESTART, OP_COUNT_ + OP_SET_TEX_POOL, OP_SET_SAMP_POOL, OP_COPY_B2T, OP_COPY_STRIDE, OP_BLEND_COLOR, OP_COPY_T2B, OP_BIND_IMG, OP_COPY_T2T, OP_RESET_COUNTER, OP_CLEAR_TEX, OP_CLEAR_BUF, OP_DEPTH_CLAMP, OP_RENDER_COND, OP_FENCE_SYNC, OP_TF_BIND, OP_TF_STATE, OP_RAST_DISCARD, OP_VP_SWIZZLE, OP_BIND_TEX1, OP_PRIM_RESTART, OP_BIND_UBO_SMALL, OP_BIND_VBUF1, OP_COUNT_ }; // Payload (immediately follows op_hdr) typedef struct { int32_t n; const tex_t *color[8]; view_t cview[8]; uint8_t has_cview[8]; const tex_t *depth; view_t dview; uint8_t has_dview; } p_set_rt; @@ -121,7 +121,22 @@ typedef struct { int32_t kind; uint8_t bytes[0x18]; } p_bind_state; // kind typedef struct { int32_t first, n; uint32_t st[16]; } p_bind_vattr; typedef struct { int32_t first, n; vs_st st[16]; } p_bind_vstream; typedef struct { int32_t first, n; NVNbufferRange r[16]; } p_bind_vbuf; +// Games measure scratch recordings to size resource heaps. Keep a single bounded binding at 24 bytes including its header (the native vertex-buffer command is 24 bytes). +// Larger ranges retain the full record; replay expands compact records to the same state, with the full 64-bit GPU address. +typedef struct { uint32_t index, size; uint64_t addr; } p_bind_vbuf1; +static inline const p_bind_vbuf *vbuf_rec(uint32_t op, const void *a, p_bind_vbuf *t) { + if (op != OP_BIND_VBUF1) return a; + const p_bind_vbuf1 *b = a; + t->first = (int32_t)b->index; t->n = 1; t->r[0] = (NVNbufferRange){b->addr, b->size}; return t; +} typedef struct { int32_t stage, index; uint64_t addr, size; } p_bind_buf; // UBO / SSBO +// Uniform bindings likewise take 24 bytes instead of 32 (the native inline command takes 20). See memory.md, model heaps sized by command records. +typedef struct { uint16_t stage, index; uint32_t size; uint64_t addr; } p_bind_ubo_small; +static inline const p_bind_buf *bind_buf_rec(uint32_t op, const void *a, p_bind_buf *t) { + if (op != OP_BIND_UBO_SMALL) return a; + const p_bind_ubo_small *b = a; + *t = (p_bind_buf){b->stage, b->index, b->addr, b->size}; return t; +} // defer / src: the handles of BindTexturesDeferred are read from src at execution time (h at recording time is only a fallback); the replay layer refreshes them with bh_fresh before reading the record (nvn_mtl.m) typedef struct { int32_t stage, first, n, defer; uint64_t src; uint64_t h[32]; } p_bind_handles; // Separate texture / sampler handles // nvnCommandBufferBindTexture (one combined handle) is laid out as the hardware's command: the handle 16 bytes from the start of the record, where an engine that patches its recorded commands diff --git a/dm/gfx/tests/t_nvn.c b/dm/gfx/tests/t_nvn.c index 8834a6c..27b489b 100644 --- a/dm/gfx/tests/t_nvn.c +++ b/dm/gfx/tests/t_nvn.c @@ -247,6 +247,96 @@ int main(void) { EXPECT("the bind reads the handle patched at its start + 0x10: slot 3, texture 1235, sampler 511", 1, rh.op == OP_BIND_TEX1 && bh->n == 1 && bh->first == 3 && bh->stage == NVN_SHADER_STAGE_FRAGMENT && bh->h[0] == patched); P(nvnCommandBufferFinalize)(&c2); P(nvnMemoryPoolFinalize)(&mp); } + // Uniform bindings are measured into resource budgets too. A bounded binding needs + // 8 header bytes plus 16 payload bytes; keep the full record for larger ranges. + { + static uint8_t m[4096] __attribute__((aligned(4096))); NVNmemoryPoolBuilder mb; NVNmemoryPool mp; NVNcommandBuffer c2; + P(nvnMemoryPoolBuilderSetDefaults)(&mb); P(nvnMemoryPoolBuilderSetStorage)(&mb, m, sizeof m); P(nvnMemoryPoolInitialize)(&mp, &mb); + P(nvnCommandBufferInitialize)(&c2, &dev); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 0, sizeof m); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindUniformBuffer)(&c2, NVN_SHADER_STAGE_FRAGMENT, 13, UINT64_C(0x123456789abc0000), 65536); + NVNcommandHandle first = P(nvnCommandBufferEndRecording)(&c2); + size_t first_size = P(nvnCommandBufferGetCommandMemoryUsed)(&c2); + EXPECT("maximum uniform range: 24-byte bind + END", 32, first_size); + op_hdr rh; memcpy(&rh, m, sizeof rh); + EXPECT("bounded uniform record is 24 bytes", 24, rh.size); + p_bind_buf decoded; const p_bind_buf *b = bind_buf_rec(rh.op, m + 8, &decoded); + EXPECT("bounded uniform decode keeps stage, index, address, size", 1, + b->stage == NVN_SHADER_STAGE_FRAGMENT && b->index == 13 && b->addr == UINT64_C(0x123456789abc0000) && b->size == 65536); + op_hdr end; memcpy(&end, m + rh.size, sizeof end); + EXPECT("uniform bind preserves its adjacent END", 1, end.op == OP_END && end.size == 8 && (const uint8_t *)first == m); + uint8_t saved[64]; memcpy(saved, m, first_size); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, (ptrdiff_t)first_size, sizeof m - first_size); P(nvnCommandBufferBeginRecording)(&c2); + NVNbufferRange ranges[] = {{UINT64_C(0xfedcba9876540000), 256}, {UINT64_C(0x1122334455660000), 0}}; + P(nvnCommandBufferBindUniformBuffers)(&c2, NVN_SHADER_STAGE_VERTEX, 2, 2, ranges); + P(nvnCommandBufferEndRecording)(&c2); + memset(ranges, 0, sizeof ranges); + EXPECT("plural uniform binds: 2 x 24 + END", 56, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + EXPECT("independent uniform recording preserves predecessor", 0, memcmp(saved, m, first_size)); + size_t pos = first_size; + memcpy(&rh, m + pos, sizeof rh); b = bind_buf_rec(rh.op, m + pos + 8, &decoded); + EXPECT("plural first uniform keeps copied range and slot", 1, + b->stage == NVN_SHADER_STAGE_VERTEX && b->index == 2 && b->addr == UINT64_C(0xfedcba9876540000) && b->size == 256); + pos += rh.size; + memcpy(&rh, m + pos, sizeof rh); b = bind_buf_rec(rh.op, m + pos + 8, &decoded); + EXPECT("plural second uniform preserves zero range and next slot", 1, + b->stage == NVN_SHADER_STAGE_VERTEX && b->index == 3 && b->addr == UINT64_C(0x1122334455660000) && b->size == 0); + memcpy(&end, m + pos + rh.size, sizeof end); + EXPECT("plural uniform binds preserve their END", 1, end.op == OP_END && end.size == 8); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 512, sizeof m - 512); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindUniformBuffer)(&c2, NVN_SHADER_STAGE_COMPUTE, 3, UINT64_C(0xffff123456780000), UINT64_C(0x100000001)); + P(nvnCommandBufferEndRecording)(&c2); + memcpy(&rh, m + 512, sizeof rh); + EXPECT("wide uniform range keeps full record + END", 40, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + EXPECT("wide uniform range keeps full opcode and size", 1, rh.op == OP_BIND_UBO && rh.size == 32); + b = bind_buf_rec(rh.op, m + 520, &decoded); + EXPECT("wide uniform decode preserves all 64 bits", 1, + b->stage == NVN_SHADER_STAGE_COMPUTE && b->index == 3 && b->addr == UINT64_C(0xffff123456780000) && b->size == UINT64_C(0x100000001)); + P(nvnCommandBufferFinalize)(&c2); P(nvnMemoryPoolFinalize)(&mp); + } + // One vertex range needs 8 header bytes plus 16 payload bytes; arrays and + // ranges wider than 32 bits retain the general representation. + { + static uint8_t m[4096] __attribute__((aligned(4096))); NVNmemoryPoolBuilder mb; NVNmemoryPool mp; NVNcommandBuffer c2; + P(nvnMemoryPoolBuilderSetDefaults)(&mb); P(nvnMemoryPoolBuilderSetStorage)(&mb, m, sizeof m); P(nvnMemoryPoolInitialize)(&mp, &mb); + P(nvnCommandBufferInitialize)(&c2, &dev); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 0, sizeof m); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindVertexBuffer)(&c2, 15, UINT64_C(0xfedcba9876540000), UINT32_MAX); + P(nvnCommandBufferEndRecording)(&c2); + EXPECT("single vertex buffer: 24-byte bind + END", 32, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + op_hdr rh, end; memcpy(&rh, m, sizeof rh); memcpy(&end, m + rh.size, sizeof end); + EXPECT("single vertex binding record is 24 bytes", 24, rh.size); + EXPECT("single vertex binding preserves adjacent END", 1, end.op == OP_END && end.size == 8); + p_bind_vbuf tmp; const p_bind_vbuf *b = vbuf_rec(rh.op, m + 8, &tmp); + EXPECT("single vertex decode preserves slot, full address, maximum size", 1, + b->first == 15 && b->n == 1 && b->r[0].address == UINT64_C(0xfedcba9876540000) && b->r[0].size == UINT32_MAX); + NVNbufferRange ranges[] = {{UINT64_C(0x1122334455660000), 0}, {UINT64_C(0x9988776655440000), 256}}; + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 128, sizeof m - 128); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindVertexBuffers)(&c2, 3, 1, ranges); P(nvnCommandBufferEndRecording)(&c2); + EXPECT("one-element vertex array: 24-byte bind + END", 32, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + memcpy(&rh, m + 128, sizeof rh); b = vbuf_rec(rh.op, m + 136, &tmp); + EXPECT("one-element vertex array decode preserves zero range", 1, + b->first == 3 && b->n == 1 && b->r[0].address == UINT64_C(0x1122334455660000) && b->r[0].size == 0); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 256, sizeof m - 256); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindVertexBuffers)(&c2, 4, 2, ranges); P(nvnCommandBufferEndRecording)(&c2); + memset(ranges, 0, sizeof ranges); + EXPECT("two vertex ranges retain array record + END", 56, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + memcpy(&rh, m + 256, sizeof rh); + EXPECT("two vertex ranges retain array opcode", OP_BIND_VBUF, rh.op); + b = vbuf_rec(rh.op, m + 264, &tmp); + EXPECT("vertex array decode preserves both copied ranges", 1, + b->first == 4 && b->n == 2 && b->r[0].address == UINT64_C(0x1122334455660000) && !b->r[0].size && + b->r[1].address == UINT64_C(0x9988776655440000) && b->r[1].size == 256); + P(nvnCommandBufferAddCommandMemory)(&c2, &mp, 384, sizeof m - 384); P(nvnCommandBufferBeginRecording)(&c2); + P(nvnCommandBufferBindVertexBuffer)(&c2, 7, UINT64_C(0xabcd123456780000), UINT64_C(0x100000001)); P(nvnCommandBufferEndRecording)(&c2); + EXPECT("wide vertex range retains full record + END", 40, P(nvnCommandBufferGetCommandMemoryUsed)(&c2)); + memcpy(&rh, m + 384, sizeof rh); + EXPECT("wide vertex range retains array opcode", OP_BIND_VBUF, rh.op); + b = vbuf_rec(rh.op, m + 392, &tmp); + EXPECT("wide vertex range decode preserves all 64 bits", 1, + b->first == 7 && b->n == 1 && b->r[0].address == UINT64_C(0xabcd123456780000) && b->r[0].size == UINT64_C(0x100000001)); + P(nvnCommandBufferFinalize)(&c2); P(nvnMemoryPoolFinalize)(&mp); + } // 5b'. Fixed slots sized by GetCommandMemoryUsed, as Tears of the Kingdom gives each model's prerecorded lists (GitHub issue #61): a list measured at S bytes is recorded // into slot k = base + S × k with AddCommandMemory(max(S, 512)), and slot k + 1 is recorded right after it. The list here is an uniform update of n bytes and a // uniform buffer bind (the last record before the END in the crash), the next slot's starts with a CALL. Walking slot 0 afterwards must reach its own END through @@ -275,7 +365,13 @@ int main(void) { if (h.op == OP_JUMP) { const uint8_t *t; memcpy(&t, p + 8, 8); int in = t >= m && t < m + sizeof m; for (int i = 0; i < g_nhostseg; i++) in |= t >= g_hostseg[i].mem && t < g_hostseg[i].mem + g_hostseg[i].n; if (!in) { ok = 0; break; } jumped = 1; p = t; continue; } - if (k < 4) ops[k] = (int)h.op; k++; + uint32_t op = h.op; + if (op == OP_BIND_UBO || op == OP_BIND_UBO_SMALL) { + p_bind_buf tmp; const p_bind_buf *b = bind_buf_rec(op, p + 8, &tmp); + ok &= b->stage == NVN_SHADER_STAGE_FRAGMENT && b->index == 4 && b->addr == 0x20000 && b->size == 0x100; + op = OP_BIND_UBO; + } + if (k < 4) ops[k] = (int)op; k++; if (h.op == OP_END) break; p += h.size; } @@ -285,7 +381,7 @@ int main(void) { } printf("(first broken slot size %d)\n", first_bad); EXPECT("slotted lists replay whole after the next slot is recorded", 0, broken); - EXPECT("lists of 64..504 bytes stay in their 512 (no host segment)", (504 - 64) / 8 + 1, jumps_fit); + EXPECT("lists of 56..504 bytes stay in their 512 (no host segment)", (504 - 56) / 8 + 1, jumps_fit); P(nvnCommandBufferFinalize)(&c3); P(nvnMemoryPoolFinalize)(&mp); } // 5b''. Host segment sizes (hostseg_size), as Super Mario Odyssey grants 512 bytes from an empty pool before recording: such a grant gets a HOSTSEG_SMALL segment; a recording diff --git a/dm/gfx/tests/t_reorder_chain.m b/dm/gfx/tests/t_reorder_chain.m index b6fbbd8..65fdd63 100644 --- a/dm/gfx/tests/t_reorder_chain.m +++ b/dm/gfx/tests/t_reorder_chain.m @@ -129,16 +129,22 @@ static uint64_t query_case(int reordered, int mode) { const char *names[]={"query","same_range_query","later_reset_overrides_query"}; save_raw(names[mode],reordered,q,sizeof q); mtl_pool_del(c.p); free(c.p); return q[0]; } -static void uniform_case(int reordered,uint8_t pixels[256]) { +static void uniform_case(int reordered,int compact,uint8_t pixels[256]) { void *mem=NULL; if(posix_memalign(&mem,getpagesize(),getpagesize())) abort(); memset(mem,0,getpagesize()); float red[4]={1,0,0,1},green[4]={0,1,0,1}; memcpy(mem,red,16); mtl_pool_add(mem,getpagesize()); tex_t a=texture(8,1,0),b=texture(8,1,0),out=texture(8,1,0),e=texture(8,1,0); chain c=make_chain(); clear(&c,&out,0,NULL,0,0,0); clear(&c,&e,0,NULL,0,0,0); stencil(&c,0,8,1,255,255,0); blending(&c,0); - p_bind_buf ub={.stage=1,.index=0,.addr=(uint64_t)mem,.size=16}; emit(&c,OP_BIND_UBO,&ub,sizeof ub); + if(compact) { + p_bind_ubo_small ub={.stage=1,.index=0,.addr=(uint64_t)mem,.size=16}; emit(&c,OP_BIND_UBO_SMALL,&ub,sizeof ub); + } else { + p_bind_buf ub={.stage=1,.index=0,.addr=(uint64_t)mem,.size=16}; emit(&c,OP_BIND_UBO,&ub,sizeof ub); + } if(!reordered) { clear(&c,&a,0,NULL,0,0,0); draw_prog(&c,4,0); } struct { uint64_t dst,n; float color[4]; } update={(uint64_t)mem,16,{0}}; memcpy(update.color,green,16); emit(&c,OP_UPDATE_UB,&update,sizeof update); if(reordered) { clear(&c,&a,0,NULL,0,0,0); draw_prog(&c,4,0); } clear(&c,&b,0,NULL,0,0,0); draw_prog(&c,4,0); result r=execute(&c,&a,&b,&out,&e,NULL); save_result("UB_write",reordered,&r); memcpy(pixels,r.a,256); + EXPECT("uniform replay restores canonical stage and slot",1,S.ubo[1][0].stage==1&&S.ubo[1][0].index==0); + EXPECT("uniform replay restores full address and size",1,S.ubo[1][0].addr==(uint64_t)mem&&S.ubo[1][0].size==16); uniform_pixels("second draw after the UB update always green",r.b,0,255,0,255); mtl_pool_del(mem); free(mem); } // A view's accessible range cannot be judged by base alone: the shader reads relative to the second level/second layer, and G writes that subresource. @@ -253,7 +259,11 @@ int main(int argc,char **argv) { @autoreleasepool { } uint64_t q0=query_case(0,0),q1=query_case(1,0); EXPECT("reset original range 4x4 samples passed",16,q0); EXPECT("across-reset negative 4x4+8x8 samples passed",80,q1); EXPECT("query lifetime must not count as independent",all_independent?1:0,q0==q1); - uint8_t u0[256],u1[256]; uniform_case(0,u0); uniform_case(1,u1); uniform_pixels("UB original order first draw red",u0,255,0,0,255); uniform_pixels("across UB write first draw green",u1,0,255,0,255); + uint8_t u0[256],u1[256],uc0[256],uc1[256]; uniform_case(0,0,u0); uniform_case(1,0,u1); + uniform_case(0,1,uc0); uniform_case(1,1,uc1); + uniform_pixels("UB original order first draw red",u0,255,0,0,255); uniform_pixels("across UB write first draw green",u1,0,255,0,255); + EXPECT("compact UB original order matches full record pixels",0,memcmp(u0,uc0,256)); + EXPECT("compact UB reordered matches full record pixels",0,memcmp(u1,uc1,256)); EXPECT("UB write must not move across draws",all_independent?1:0,!memcmp(u0,u1,256)); for(int array=0;array<2;array++) { const char *name=array?"whole_slice_range":"whole_mip_range"; printf("CASE %s\n",name); result o=range_case(array,0),r=range_case(array,1); uniform_pixels("range original order second subresource blue",o.b,0,0,255,255); diff --git a/dm/gfx/tests/t_vbuf.m b/dm/gfx/tests/t_vbuf.m index cce412b..4c34295 100644 --- a/dm/gfx/tests/t_vbuf.m +++ b/dm/gfx/tests/t_vbuf.m @@ -42,9 +42,20 @@ int main(void) { @autoreleasepool { memset(mem, 0, size); mem[0] = 11; mem[1] = 22; mem[2] = 33; mem[3] = 99; mtl_pool_add(mem, size); id out = [g_dev newBufferWithLength:16 * 8 options:MTLResourceStorageModeShared]; res_add(out); - state_reset(); S.vbuf[0].address = (uint64_t)mem; S.vbuf[14].address = (uint64_t)(mem + 1); S.vbuf[15].address = (uint64_t)(mem + 2); + state_reset(); + // Exercise the compact record through the production dispatcher before the + // existing shader reads each stream. Its public state remains a full range. + struct { op_hdr h; p_bind_vbuf1 b; } bindings[4] = { + {{OP_BIND_VBUF1,24},{0,4,(uint64_t)mem}}, + {{OP_BIND_VBUF1,24},{14,4,(uint64_t)(mem+1)}}, + {{OP_BIND_VBUF1,24},{15,4,(uint64_t)(mem+2)}}, + {{OP_END,8},{0}} + }; uint64_t other, off; void *mb; mtl_gpu_addr((uint64_t)(mem + 3), 1, &mb, &off, &other); - mtl_sub_begin(); id en = encoder(ps); + mtl_sub_begin(); mtl_exec((const uint8_t *)bindings); + EXPECT("compact vertex replay restores first full range",1,S.vbuf[0].address==(uint64_t)mem&&S.vbuf[0].size==4); + EXPECT("compact vertex replay restores high stream range",1,S.vbuf[15].address==(uint64_t)(mem+2)&&S.vbuf[15].size==4); + id en = encoder(ps); bind_vbufs(en); capture(en, out, 0); uint64_t resolves = g_vbuf_resolve, sets = g_vbuf_set; for (int i = 0; i < 1000; i++) bind_vbufs(en); diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 5fdcf5b..8b0865f 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -56,8 +56,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - the alias region's mapped total runs past the application pool in the first minute. → [Maps past the application pool fail as on Horizon](../reference/memory.md#maps-past-the-application-pool-fail-as-on-horizon) - a write through a null pointer at +0x1c0 during startup. → [`GetInfo(UserExceptionContextAddress)` returns a real buffer](../reference/memory.md#getinfouserexceptioncontextaddress-returns-a-real-buffer) - a null-pool buffer even with virtual pools working (Virtual memory pools mapped onto one physical pool). → [A null-pool buffer from a game heap that ran out](../reference/memory.md#a-null-pool-buffer-from-a-game-heap-that-ran-out) -- a null-pool buffer for `Model/NpcCommonerA.bfres` in the opening course. → [Per-resource heaps sized from Nintendo's measured peaks](../reference/memory.md#per-resource-heaps-sized-from-nintendos-measured-peaks) -- `main+0x155a7a4` writes through a null `operator new` at the world load, about 1 run in 3. → [Model heaps sized by the host's command record sizes](../reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes) +- a model load exhausts its resource heap: a null `operator new` at the world load, or `BufferInitialize: pool has no storage` followed by a null-pool crash. → [Model heaps sized by the host's command record sizes](../reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes) - the frames stop at the first loading screen, a graphics worker at 100% CPU (17 s in, `UnityGfxDeviceWorker` running, the main thread waiting on a condition variable, no red line). → [Unity's GPU pool block allocation spins at size 0](../reference/memory.md#unitys-gpu-pool-block-allocation-spins-at-size-0) - SIGBUS inside a host hook (`snprintf` under `mount_ex` / `h_fs_MountSave`), then the process hangs (Super Mario 3D World + Bowser's Fury); SIGBUS inside Metal (`AGCDeserializedReply`, an AGX compiler block) at an address in the guest stack region (Breath of the Wild). → [Host calls on small guest stacks](../reference/memory.md#host-calls-on-small-guest-stacks) - the iPhone app is killed for memory; resident memory grows steadily (about 0.2 MB a frame) while the residency set and the texture counts stay flat. → [Deferred-release drain without an autorelease pool](../reference/memory.md#deferred-release-drain-without-an-autorelease-pool) diff --git a/docs/reference/memory.md b/docs/reference/memory.md index 2a0dca4..6c5d201 100644 --- a/docs/reference/memory.md +++ b/docs/reference/memory.md @@ -232,21 +232,13 @@ The guest address space, heaps, pools, virtual address memory and the allocators - **Note:** read the probe log carefully: `tryAlloc` probes show *requests*, not successes, so a running total that already exceeds the heap means an earlier request failed. - **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md), [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md) (the two entries below). -### Per-resource heaps sized from Nintendo's measured peaks - -- **Symptom:** a null-pool buffer for `Model/NpcCommonerA.bfres` in the opening course. -- **Cause:** each resource gets its own `sead::ExpHeap` sized *resource-size-table value + 0x1008* (`main+0x1e603c` → `ExpHeap::create` `main+0x4aac38`). The table values are Nintendo's measured peaks with almost no margin (Yoshi's animation file uses 99 % of its budget). `Model/NpcCommonerA.bfres` came up about 0x70 bytes short: its last GPU chunk (0x9000 bytes, 4 KB aligned) fit, but the chunk's 0x598-byte descriptor did not. -- **Fix:** a registered code cut gives every resource heap +16 KB (`CUTS` in the profile's `patches.py`, keyed by the module build ID, original bytes verified). Per-title code patches go there, with the reason; those of Nintendo's `sdk` modules in `dm/tools/sdk_patches.py`. -- **Note:** Breath of the Wild's identical failure came from sizes the game takes from the host (next entry); whether Mario's chunk is sized the same way is not checked yet. -- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md). - ### Model heaps sized by the host's command record sizes -- **Symptom:** `main+0x155a7a4` writes through a null `operator new` at the world load, about 1 run in 3. -- **Cause:** each model's heap is its table entry + 0x1008, the file goes in 4 KB aligned, and the setup (`main+0x155a2ac`) takes the rest as a frame heap (vtable `main+0x1d4f950`, head `+0xe0`, tail `+0xe8`). Its use is the same in every run; only the file's alignment pad varies (0 to 0xff8), and the entries leave room for any pad, about 0xd00 to spare on most models. Three models were a 4 KB page short, so the loads with a large pad failed. The game sizes the model's prerecorded commands by recording each mesh's draw and each shape's per-pass state into a scratch command buffer and summing `GetCommandMemoryUsed` (`main+0xf9aeb4`, `main+0xf9bb54`), and our records were larger than the driver's (draws carried the indirect and multi-draw fields, every recording began with a BEGIN record). -- **Fix:** plain draws now leave those fields out and recordings start with their first command (`hle_nvn.c`; [What `GetCommandMemoryUsed` counts](nvn-objects.md#what-getcommandmemoryused-counts)). -- **Check:** to check a whole scene, probe where the setup heap is cut (`main+0x155a520` with `!x21`, the resource heap) and the end of the setup (`main+0x155a5f4` with `!x2`, the frame heap), with `DM_PROBE_HEAP=28,38,40,e0,f8 DM_PROBE_HEAPFRAME=1d4f950 DM_PROBE_HEAPWORDS=1` ([05](../agents/05-debugging.md#probes-without-a-debugger-dm_probe)). A model fits any placement when entry + 0x1008 covers the resource heap's header and object (0x310), the file, the frame heap's block header and its peak use, plus 0xff8 of pad. -- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md). +- **Symptom:** a model load exhausts its resource heap: a null `operator new` at the world load, or `BufferInitialize: pool has no storage` followed by a null-pool crash. +- **Cause:** games size model allocations by measuring scratch command recordings; oversized host records can exceed the budgets in their resource-size tables. In Breath of the Wild, each model's heap is its table entry + 0x1008, the file goes in 4 KB aligned, and the setup (`main+0x155a2ac`) takes the rest as a frame heap (vtable `main+0x1d4f950`, head `+0xe0`, tail `+0xe8`). Its use is the same in every run; only the file's alignment pad varies (0 to 0xff8), and the entries leave room for any pad, about 0xd00 to spare on most models. Three models were a 4 KB page short, so the loads with a large pad failed. The game sizes the model's prerecorded commands by recording each mesh's draw and each shape's per-pass state into a scratch command buffer and summing `GetCommandMemoryUsed` (`main+0xf9aeb4`, `main+0xf9bb54`), and our records were larger than the driver's (draws carried the indirect and multi-draw fields, every recording began with a BEGIN record). +- **Fix:** plain draws leave unused indirect fields out and recordings start with their first command. Uniform and single vertex-buffer bindings also use compact 24-byte records instead of 32, preserving full GPU addresses and retaining full-width fallback records for ranges above `UINT32_MAX`; multi-vertex ranges retain their array representation. Replay expands the compact forms into the same canonical binding state. The native inline uniform command takes 20 bytes and the native vertex-buffer command 24, so these small differences can accumulate into an extra 4 KB GPU-pool chunk. Report the bytes actually recorded; do not underreport usage or add a game-specific heap allowance. See [What `GetCommandMemoryUsed` counts](nvn-objects.md#what-getcommandmemoryused-counts). +- **Check:** for Breath of the Wild's whole scene, probe where the setup heap is cut (`main+0x155a520` with `!x21`, the resource heap) and the end of the setup (`main+0x155a5f4` with `!x2`, the frame heap), with `DM_PROBE_HEAP=28,38,40,e0,f8 DM_PROBE_HEAPFRAME=1d4f950 DM_PROBE_HEAPWORDS=1` ([05](../agents/05-debugging.md#probes-without-a-debugger-dm_probe)). A model fits any placement when entry + 0x1008 covers the resource heap's header and object (0x310), the file, the frame heap's block header and its peak use, plus 0xff8 of pad. +- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md), [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md#memory) (buffer bindings; the game note records the failed allocation, measured sizes and validation). ### Unity's GPU pool block allocation spins at size 0 diff --git a/docs/reference/rewriter.md b/docs/reference/rewriter.md index 7297b7c..9c14e9c 100644 --- a/docs/reference/rewriter.md +++ b/docs/reference/rewriter.md @@ -11,13 +11,13 @@ The NSO rewriter (`dm/tools/patch_nso.py`) and its per-build tables (`dm/tools/s - **Fix:** a profile can opt individual cuts into `MATCH_CUTS` in its `patches.py`. The shared matcher, `dm/tools/nso_match.py`, selects a unique complete unwind function by length and SHA-256 fingerprint. It retains constants, registers and internal branch destinations; normalizes relocation addresses; and checks the normalized bodies of direct callees or their imported symbol names. Original bytes are checked again before a cut is applied. Missing boundaries, unknown callees, changed code and ambiguous matches produce a red diagnostic and no cut. Overlapping rules stop the rewrite. Every accepted cut is recorded in `.cuts.tsv` with `[function match]` in its reason. - **Scope:** the repository profile selected by `DM_GAME`, keyed by module name. A derived profile still uses that repository profile's rules. Other games' rules never participate. A build with registered `CUTS` uses those instead, including an explicitly empty list. SDK patches, observation gates, frame-step stubs and fixed host addresses retain their existing build-ID gates. The rewrite cache includes the selected profile's patch file even for unknown builds, the selected game ID and the matching tool. - **Limits:** this is an opt-in code-identity check, not automatic compatibility for every game update. Direct callee bodies are checked one level deep; their callees, indirect-call targets and external data layouts are not proven unchanged. Enroll only a cut whose assumptions remain valid under those changes. Prefer fixing the general host behavior when possible. A replacement must contain no PC-relative instruction; relocated branch replacements need a different mechanism. Failure leaves the game potentially without its workaround, and stays visible in the rewrite log. -- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md#memory), whose resource-heap allowance was missing on 1.0.1. +- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md#memory), whose former resource-heap allowance was missing on 1.0.1. The shared matcher remains, but Wonder's allowance and matching rule were later removed after the general command-record fix described in its game note. To enroll a cut for any game's own module: 1. Start from a verified fixed cut and its original NSO. Run `python3 dm/tools/nso_match.py ` with the tools' Python environment. It emits a rule containing `name`, `size`, `fingerprints` and function-relative `cuts`, and rejects an ambiguous reference. Keep game files and scratch outputs in the game's NSP work folder. 2. Check the function and callees in another known build using `nso_fnmap.py` and disassembly. Check that the cut still has the same effect. `Image.match(rule)` must resolve the reviewed location. If a reviewed implementation differs, generate its fingerprint separately; combine fingerprints only when function size and relative cuts agree. Retain exact `CUTS` for known builds. -3. Add the rule under `MATCH_CUTS = {'main': [rule]}` (or the appropriate non-SDK module) in that game's `patches.py`. Record the evidence and remaining runtime validation in the game's note. Do not copy Wonder's rule or allowance into unrelated games. +3. Add the rule under `MATCH_CUTS = {'main': [rule]}` (or the appropriate non-SDK module) in that game's `patches.py`. Record the evidence and remaining runtime validation in the game's note. Do not transplant a workaround into unrelated games. 4. Verify an unregistered build-ID copy through the rewriter, comparing its output with the known fixed cut, and run `test_nso_match.py`, `test_patches.py` and `test_nro_pack.py`. The matching tests use synthetic modules from two profiles to check relocation, rejection and isolation without game files. ## Jump tables From 2d344f050c853aeb363043658576b6ebf9d4a8f8 Mon Sep 17 00:00:00 2001 From: seedds Date: Mon, 26 Oct 2026 01:25:40 +0800 Subject: [PATCH 4/6] Test NVN readback lifetime and document general Metal fixes --- dm/games/01007EF00011E000/README.md | 1 + dm/games/0100F2C0115B6000/README.md | 1 + dm/gfx/nvn/nvn_mtl.m | 13 ++++++------- dm/gfx/tests/t_readback.m | 19 +++++++++++++++++++ docs/agents/symptoms.md | 3 +++ docs/reference/frame-rate.md | 16 ++++++++++++++++ docs/reference/memory.md | 8 ++++++++ 7 files changed, 54 insertions(+), 7 deletions(-) diff --git a/dm/games/01007EF00011E000/README.md b/dm/games/01007EF00011E000/README.md index 06329a6..3367094 100644 --- a/dm/games/01007EF00011E000/README.md +++ b/dm/games/01007EF00011E000/README.md @@ -110,6 +110,7 @@ Used at `DM_FPS_TARGET=0` and for the interval the framework keeps while the VFR General host changes first needed here; each applies to every game: +- **Resolved: GPU readback temporaries retained by a game thread's outer pool** (`mtl_read_texels`): [GPU readback without an autorelease pool](../../../docs/reference/memory.md#gpu-readback-without-an-autorelease-pool). Ported from MacNativeGames; checked here with a synthetic Metal readback lifetime test, without a new gameplay memory measurement. - **The host frame step in whole vsyncs** (`nvn_fstep.h`, `fstep_tick`, `FRAME_STEP` stubs, `fps_target`), explained in full under [Frame rate](#the-frame-step-current); its probes (`nvn_fstep_gpu_slowed`, `nvn_fstep_same`): [development.md, Power](../../../docs/development.md#power). - **Real GPU timestamps per segment** (`gpu_timestamps`, `rep_ticks`), **submissions in segments** (`sub_cut`), **deferred texture binds** (`bh_fresh`), **duplicate colour slots** (`rt_dup_slot`), **cube textures as 2D arrays** (`tex_fit`), **compressed render targets through shadows** (`bcrt_attach` / `bcrt_flush`), **pool lookups** (`pool_find`), **memory fixes** (`defer_drain`'s pool, `g_ghost`, growing pool table), **`nvnTextureCompare` with the channel mapping**, **first-use fill of flushed storage** (`nvn_flushed`, `tex_init_fill`), **partial-cover uniform shadows** (`ubp_get`, `cb_extent.tsv`): [GPU timestamps per submission segment](../../../docs/reference/frame-rate.md#gpu-timestamps-per-submission-segment), [Submissions handed to the GPU in segments](../../../docs/reference/frame-rate.md#submissions-handed-to-the-gpu-in-segments), [Deferred texture binds read at execution](../../../docs/reference/shaders.md#deferred-texture-binds-read-at-execution), [The same texture attached at two colour slots](../../../docs/reference/rendering.md#the-same-texture-attached-at-two-colour-slots), [Cube textures held as 6-layer 2D arrays](../../../docs/reference/rendering.md#cube-textures-held-as-6-layer-2d-arrays), [Compressed textures as render targets through an uncompressed shadow](../../../docs/reference/rendering.md#compressed-textures-as-render-targets-through-an-uncompressed-shadow), [Address to memory pool lookups by binary search](../../../docs/reference/frame-rate.md#address-to-memory-pool-lookups-by-binary-search), [Deferred-release drain without an autorelease pool](../../../docs/reference/memory.md#deferred-release-drain-without-an-autorelease-pool), [Dead texture serials kept in a ghost table](../../../docs/reference/nvn-objects.md#dead-texture-serials-kept-in-a-ghost-table), [Memory-pool tracking table grows in blocks](../../../docs/reference/nvn-objects.md#memory-pool-tracking-table-grows-in-blocks), [`nvnTextureCompare` compares the channel mapping](../../../docs/reference/rendering.md#nvntexturecompare-compares-the-channel-mapping), [First-use fill of storage the game flushed](../../../docs/reference/nvn-objects.md#first-use-fill-of-storage-the-game-flushed), [Partial-cover uniform shadows keep passes open](../../../docs/reference/rendering.md#partial-cover-uniform-shadows-keep-passes-open). - **Display-link waits per present interval** (`mtl_link_wait_tick`): [Frames of a dynamic present interval held on screen and at the acquire](../../../docs/reference/frame-rate.md#frames-of-a-dynamic-present-interval-held-on-screen-and-at-the-acquire). diff --git a/dm/games/0100F2C0115B6000/README.md b/dm/games/0100F2C0115B6000/README.md index b84565f..3810497 100644 --- a/dm/games/0100F2C0115B6000/README.md +++ b/dm/games/0100F2C0115B6000/README.md @@ -68,6 +68,7 @@ Game-specific findings. The general host changes they led to are listed under [F General host changes first needed here; each applies to every game: +- **Resolved: registry lock priority inversion** (`g_rmu`): [Registry lock priority inheritance](../../../docs/reference/frame-rate.md#registry-lock-priority-inheritance). Ported from MacNativeGames; checked here through the Metal registry view tests, without a new gameplay contention measurement. - **Conditional rendering** evaluated at replay (`OP_RENDER_COND`, profile key `render_cond`) and **CPU-written textures with no flush** (`tex_dirty_upload`, `ft_add`): [Conditional rendering evaluated at replay](../../../docs/reference/rendering.md#conditional-rendering-evaluated-at-replay), [CPU writes with no flush, caught by page protection](../../../docs/reference/nvn-objects.md#cpu-writes-with-no-flush-caught-by-page-protection-tex_dirty_upload). - **Mid-pass, partial-mask and partial-scissor clears** drawn as a triangle (`clear_color_tri` / `clear_ds_tri`), **8-bit indices widened on the GPU** when written in the same submission (`wide_run`), **`ReadTexels` from guest memory on any thread** with `nvn_ft_poll` marking textures written after first use, **attachment-less passes** (`bare_dims`): same section. - **Rewriter:** out-of-line atomic retry blocks (`retry_block` / `site_retry_window`, 8 sites) and jump tables relative to an `adr` (`jump_adr_offsets`, `main+0x393bb0`): [Out-of-line retry blocks of atomic updates](../../../docs/reference/rewriter.md#out-of-line-retry-blocks-of-atomic-updates), [Offset tables relative to an adr](../../../docs/reference/rewriter.md#offset-tables-relative-to-an-adr). diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index f129f1b..2ff2b51 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -2897,9 +2897,8 @@ static int clear_rect_zero(int i) { // ================= Shaders / descriptor pool / pipelines / draws ================= -// The texture / sampler registry lock: game threads take it to register textures and samplers, the submit thread for every argument encode. An os_unfair_lock lends a -// waiter's priority to its owner: with a mutex, a game thread (priority 46) preempted while holding it kept the submit thread (97) waiting, 1.8 ms a frame in a Tears of -// the Kingdom shrine (MacNativeGames 5f5de5e: 155 -> ~0 ms/s of waiting) +// Game threads register textures and samplers, the submit thread encodes arguments: lend a waiting submitter's priority to the owner. +// Measurements: docs/reference/frame-rate.md, Registry lock priority inheritance. static os_unfair_lock g_rmu = OS_UNFAIR_LOCK_INIT; // ---------- Shader mapping table: Switch control section hash → PC metallib (produced by dm/gfx/tools/shader_map.py) ---------- static NSMutableDictionary *g_shmap; @@ -3770,8 +3769,8 @@ void arena_begin(void) { if (a->fence && a->fence <= done && a->len > ARENA_KEEP && g_fence > a->fence + ARENA_IDLE) { res_remove(a->buf); [g_arenas removeObjectAtIndex:i]; } } } void arena_commit(uint64_t fence) { for (NvArena *a in g_sub_arenas) { a->fence = fence; [g_arenas addObject:a]; } [g_sub_arenas removeAllObjects]; g_cur_arena = nil; } -// buf is __strong: an unqualified id * out-parameter is __autoreleasing under ARC, which put the arena buffer in the autorelease pool at every call (a few per draw; -// MacNativeGames 0531f28: autorelease 2.63% -> 1.73% of replay time) +// An unqualified id * out-parameter is __autoreleasing under ARC; __strong avoids autoreleasing the arena buffer at every allocation. +// Measurements: docs/reference/frame-rate.md, Arena buffer ownership on allocation. static size_t arena_alloc(size_t n, __strong id *buf) { n = (n + 255) & ~(size_t)255; if (!g_cur_arena || g_cur_arena->used + n > g_cur_arena->len) { @@ -7629,8 +7628,8 @@ void mtl_read_texels(const tex_t *t, unsigned level, unsigned layer0, const NVNc free(lin); return; } - // The game's own thread, outside a submission: no pool drains what Metal autoreleases here (the command buffer, the encoder, the read-back buffer), and the thread lives - // for good, so without one every GPU read-back stayed alive (MacNativeGames 635fb31: Breath of the Wild's 130 read-backs a second held 3.7 GB after 30 minutes) + // A guest thread can live indefinitely without draining its outer pool: release readback temporaries here, after the GPU and CPU copies finish. + // Finding: docs/reference/memory.md, GPU readback without an autorelease pool. @autoreleasepool { id mt = tex_mtl(t); if (!mt) { memset(dst, 0, img * nz); return; } diff --git a/dm/gfx/tests/t_readback.m b/dm/gfx/tests/t_readback.m index 069fd8e..f47e196 100644 --- a/dm/gfx/tests/t_readback.m +++ b/dm/gfx/tests/t_readback.m @@ -15,6 +15,21 @@ uint64_t now_ns(void) { return bp_now(); } static int cases, failed; #define EXPECT(n,w,v) do { int a=(w), b=(v); cases++; printf("%s expected %d -> got %d %s\n",n,a,b,a==b?"PASS":"FAIL"); failed+=a!=b; } while (0) +// Observe the real readback buffer without retaining it. The caller's outer pool stays open, +// as on a long-lived guest thread; ReadTexels must drain its own temporary Metal objects. +@interface ReadbackDevice : NSObject +@property(nonatomic, strong) id device; +@property(nonatomic, weak) id lastBuffer; +@property(nonatomic) NSUInteger allocations; +@end +@implementation ReadbackDevice +- (id)newBufferWithLength:(NSUInteger)length options:(MTLResourceOptions)options { + id buffer = [_device newBufferWithLength:length options:options]; + _lastBuffer = buffer; _allocations++; return buffer; +} +- (id)forwardingTargetForSelector:(SEL)selector { (void)selector; return _device; } +@end + int main(void) { @autoreleasepool { mtl_init(); void *mem = NULL; if (posix_memalign(&mem, getpagesize(), 262144)) abort(); memset(mem, 0, 262144); @@ -41,9 +56,13 @@ int main(void) { @autoreleasepool { for (int y=0; y<4; y++) { for (int x=0; x<8; x++) bad += memcmp(out+y*64+x*4, expected, 4) != 0; for (int x=32; x<64; x++) bad += out[y*64+x] != 0xEE; } EXPECT("pending clear -> GPU copy, literal pixels and untouched row padding", 0, bad); + ReadbackDevice *device = [ReadbackDevice new]; device.device = g_dev; g_dev = (id)device; uint8_t cpu[128]; memset(cpu, 0xEE, sizeof cpu); h_nvnTextureReadTexels(&tex, NULL, ®ion, cpu); bad = 0; + g_dev = device.device; for (int i=0; i<32; i++) bad += memcmp(cpu+i*4, expected, 4) != 0; EXPECT("GPU clear -> CPU ReadTexels, 32 literal pixels", 0, bad); + EXPECT("ReadTexels used the GPU readback path", 1, device.allocations); + EXPECT("ReadTexels releases its readback buffer before the caller's pool drains", 1, device.lastBuffer == nil); h_nvnTextureFinalize(&tex); mtl_pool_del(mem); free(mem); printf("READBACK cases=%d failed=%d\n", cases, failed); return failed != 0; } } diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index e145139..f9207e9 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -61,6 +61,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - the frames stop at the first loading screen, a graphics worker at 100% CPU (17 s in, `UnityGfxDeviceWorker` running, the main thread waiting on a condition variable, no red line). → [Unity's GPU pool block allocation spins at size 0](../reference/memory.md#unitys-gpu-pool-block-allocation-spins-at-size-0) - SIGBUS inside a host hook (`snprintf` under `mount_ex` / `h_fs_MountSave`), then the process hangs (Super Mario 3D World + Bowser's Fury); SIGBUS inside Metal (`AGCDeserializedReply`, an AGX compiler block) at an address in the guest stack region (Breath of the Wild). → [Host calls on small guest stacks](../reference/memory.md#host-calls-on-small-guest-stacks) - the iPhone app is killed for memory; resident memory grows steadily (about 0.2 MB a frame) while the residency set and the texture counts stay flat. → [Deferred-release drain without an autorelease pool](../reference/memory.md#deferred-release-drain-without-an-autorelease-pool) +- GPU memory grows with repeated `nvnTextureReadTexels` calls on a long-lived game thread, even after each readback completes. → [GPU readback without an autorelease pool](../reference/memory.md#gpu-readback-without-an-autorelease-pool) ## Threads @@ -211,6 +212,8 @@ One line per symptom in the [subsystem references](../reference/): what you see - a steady game whose frame-rate counter wanders between 50 and 60 on the iPhone only; the `display link up to tick` lines show about as many empty ticks as frames dropped by merging: frames arrive two in one tick and none in the next. → [Vulkan acquire and vsync event follow the display link](../reference/frame-rate.md#vulkan-acquire-and-vsync-event-follow-the-display-link) - asked for 60 Hz, the system fires the display link on every third refresh of a 165 Hz screen, 55 Hz, so games paced by it ran at 55 fps (27.5 at 30). → [Virtual 60 Hz ticks on screens that are not a multiple of 60](../reference/frame-rate.md#virtual-60-hz-ticks-on-screens-that-are-not-a-multiple-of-60) - virtual ticks keep the average, but a frame still stays up two or three refreshes in turn (13.9 / 20.8 ms at 144 Hz), and Apple's HUD showed the frame interval jumping between them. → [Fullscreen switches the screen to a rate the frame rate divides](../reference/frame-rate.md#fullscreen-switches-the-screen-to-a-rate-the-frame-rate-divides) +- autorelease work takes a measurable share of replay time, with temporary arena-buffer ownership traffic a few times per draw. → [Arena buffer ownership on allocation](../reference/frame-rate.md#arena-buffer-ownership-on-allocation) +- the submit thread waits on the texture / sampler registry lock while a lower-priority registration thread is preempted holding it. → [Registry lock priority inheritance](../reference/frame-rate.md#registry-lock-priority-inheritance) - the GPU sits idle each frame while the host translates the submission (a game that does not submit frame N+1 until frame N's GPU work is done). → [Submissions handed to the GPU in segments](../reference/frame-rate.md#submissions-handed-to-the-gpu-in-segments) - the submit thread's CPU samples dominated by `mtl_addr` / `mtl_gpu_addr` (vertex buffers in `draw`, uniform buffers in `encode_args`). → [Address to memory pool lookups by binary search](../reference/frame-rate.md#address-to-memory-pool-lookups-by-binary-search) - stalls of 30–60 ms about once a second, at 30 fps and at 60 (99th percentile 63–127 ms); the main thread waits 20–34 ms for the render thread once a second. → [Time-constraint computation declared below the thread's peaks](../reference/frame-rate.md#time-constraint-computation-declared-below-the-threads-peaks) diff --git a/docs/reference/frame-rate.md b/docs/reference/frame-rate.md index dcec665..f558c9b 100644 --- a/docs/reference/frame-rate.md +++ b/docs/reference/frame-rate.md @@ -89,6 +89,22 @@ Frame pacing: the host's frame step, engine frame limiters, present intervals an ## CPU cost and pacing +### Arena buffer ownership on allocation + +- **Symptom:** autorelease work takes a measurable share of replay time, with temporary arena-buffer ownership traffic a few times per draw. +- **Cause:** under ARC an unqualified `id *` out-parameter is `__autoreleasing`, so assigning the arena buffer in `arena_alloc` autoreleases it. Reading `buf.length` also sends an Objective-C message on each allocation and while selecting or trimming arenas. +- **Fix:** both declarations of `arena_alloc` use a `__strong` buffer out-parameter, matching every caller's strong local. `NvArena` caches its buffer length when created; allocation, reuse selection and idle trimming use that value. Buffer ownership and fence-based reuse remain the same. +- **Found on:** MacNativeGames commit `0531f28`, ported in [PR #195](https://github.com/seedds/nxnative/pull/195), reports autorelease work falling from 2.63% to 1.73% of replay time; the PR does not identify the game for that measurement. It has not been repeated here. +- **Check:** `t_ub`, `t_vbuf` and `t_geometry` exercise arena-backed uniform shadows, vertex conversion and geometry scratch on the Mac with Metal validation. + +### Registry lock priority inheritance + +- **Symptom:** the submit thread waits on the texture / sampler registry lock while a lower-priority registration thread is preempted holding it. +- **Cause:** game threads register textures and samplers under `g_rmu`, which the submit thread also takes for argument encoding. The previous mutex allowed those owners to delay the higher-priority submitter. +- **Fix:** `g_rmu` is an `os_unfair_lock`, lending a waiter's priority to its owner. All registration, lookup, cache, diagnostic and test callers use its lock API; the critical sections and lock order stay the same. +- **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md), measured upstream in MacNativeGames commit `5f5de5e` and ported in [PR #195](https://github.com/seedds/nxnative/pull/195): in a shrine, game threads at priority 46 delayed the submit thread at priority 97 by about 1.8 ms a frame. Waiting fell from 155 to about 0 ms/s there, with unchanged frame time. Those game measurements have not been repeated here. +- **Check:** `t_stencil_view`, `t_view_levels` and `t_view_swizzle` exercise production registry lookups and cached views with the new lock on the Mac. + ### Submissions handed to the GPU in segments - **Symptom:** the GPU sits idle each frame while the host translates the submission (a game that does not submit frame N+1 until frame N's GPU work is done). diff --git a/docs/reference/memory.md b/docs/reference/memory.md index 0c0078c..f250d64 100644 --- a/docs/reference/memory.md +++ b/docs/reference/memory.md @@ -270,3 +270,11 @@ The guest address space, heaps, pools, virtual address memory and the allocators - **Fix:** `defer_drain` has its own `@autoreleasepool`. - **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md): GitHub issue #9 (iPhone 16 Pro Max, handheld), killed in the first shrine. An 8 GB iPhone gives this app about 3.4 GB (resident plus `headroom to the system memory limit` stayed at 3,375 MB); resident memory grew from 2.6 GB at the shrine load until the kill. On the Mac a host without that pool grew 1 MB a frame in the shrine (GPU 3.0 → 4.4 GB in 2,400 frames); with it the shrine stays at 2.7–2.9 GB resident and 1.5–1.7 GB GPU, idle or walking (vmmap at 2.8 GB: about 1.6 GB guest memory, 0.75 GB private textures, 0.17 GB IOAccelerator). - **Note:** found on the way: dead texture serials and the fixed-size pool tracking table ([Dead texture serials kept in a ghost table](nvn-objects.md#dead-texture-serials-kept-in-a-ghost-table), [Memory-pool tracking table grows in blocks](nvn-objects.md#memory-pool-tracking-table-grows-in-blocks)). + +### GPU readback without an autorelease pool + +- **Symptom:** GPU memory grows with repeated `nvnTextureReadTexels` calls on a long-lived game thread, even after each readback completes. +- **Cause:** the GPU path in `mtl_read_texels` runs outside a submission's autorelease pool. Metal's temporary command buffer and blit encoder keep the readback buffer alive in the calling thread's undrained pool. +- **Fix:** `mtl_read_texels` encloses texture resolution, readback encoding, the GPU wait and the CPU copy in its own `@autoreleasepool`. The guest-memory path needs no Metal objects. +- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md), measured upstream in MacNativeGames commit `635fb31` and ported in [PR #195](https://github.com/seedds/nxnative/pull/195): about 130 readbacks a second retained 3.7 GB of GPU memory after 30 minutes. Those game measurements have not been repeated here. +- **Check:** `make -C dm/gfx/tests GAME=010015100B514000 check_t_readback` checks literal GPU and CPU readback pixels and observes the real readback buffer through a weak reference while the caller's outer pool remains open. Removing only the local pool leaves that buffer alive and fails the lifetime check. From a8e76bf6010b77877f24576a909c1cbd38a94849 Mon Sep 17 00:00:00 2001 From: seedds Date: Mon, 26 Oct 2026 01:28:30 +0800 Subject: [PATCH 5/6] Port Portal: Source engine NROs on NVN, shaders from game.zip - profile 01007BB017812000 (1.0.3, SDK 13.3.0): plays on the Mac at 60 fps - provisional profiles add the subsdks the NROs import (profile_guess nro_modules) - an NRO importing nvnBootstrapLoader makes an NVN game; NROs calling glslcCompile get the run-time GLSLC capture - shader_split reads zip archive members (Portal's nxcontent/nvsb GLSLC outputs) - ro::LookupModuleSymbol for a name a loaded module lacks is not red (Horizon fails it too) - AGENTS.md: a Mac gameplay test is enough to count a game as playable (65) --- AGENTS.md | 2 +- README.md | 7 +- dm/games/01007BB017812000/README.md | 52 ++++ dm/games/01007BB017812000/bf_want.tsv | 1 + dm/games/01007BB017812000/game.tsv | 37 +++ dm/games/01007BB017812000/pso_manifest.tsv | 2 + dm/games/01007BB017812000/shader_facts.tsv | 1 + dm/games/01007BB017812000/shader_map.tsv | 258 ++++++++++++++++++ .../shader_map.tsv.conflicts.tsv | 2 + dm/games/01007BB017812000/shader_pairs.tsv | 230 ++++++++++++++++ dm/games/README.md | 3 +- dm/games/profile_guess.py | 75 ++++- dm/gfx/tools/shader_split.py | 41 ++- dm/host/hle_ro.c | 5 +- dm/tools/tests/test_nro_graphics.py | 63 +++++ docs/agents/symptoms.md | 4 + docs/images/portal.jpg | Bin 0 -> 88746 bytes docs/reference/rendering.md | 7 + docs/reference/services.md | 14 + docs/reference/shaders.md | 7 + scripts/build_ipa_from_nsp.py | 26 +- 21 files changed, 812 insertions(+), 25 deletions(-) create mode 100644 dm/games/01007BB017812000/README.md create mode 100644 dm/games/01007BB017812000/bf_want.tsv create mode 100644 dm/games/01007BB017812000/game.tsv create mode 100644 dm/games/01007BB017812000/pso_manifest.tsv create mode 100644 dm/games/01007BB017812000/shader_facts.tsv create mode 100644 dm/games/01007BB017812000/shader_map.tsv create mode 100644 dm/games/01007BB017812000/shader_map.tsv.conflicts.tsv create mode 100644 dm/games/01007BB017812000/shader_pairs.tsv create mode 100644 dm/tools/tests/test_nro_graphics.py create mode 100644 docs/images/portal.jpg diff --git a/AGENTS.md b/AGENTS.md index c5c4c9a..ab0e848 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Notes for agents -- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it (`dm/tools/patch_nso.py`), on a host that answers Horizon's SVCs and the SDK's services (`dm/host`) and implements NVN, Vulkan and NVIDIA's desktop OpenGL on Metal (`dm/gfx/nvn`, `dm/gfx/vk`, `dm/gfx/gl`). Sixty-two games are playable on the iPhone; the [games table](dm/games/README.md#games) lists each with its build, SDK, engine and graphics API, and links its note, `dm/games//README.md`: what its profile needs and why, what was found on it, how to drive it on the Mac, what is open. [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and power. +- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it (`dm/tools/patch_nso.py`), on a host that answers Horizon's SVCs and the SDK's services (`dm/host`) and implements NVN, Vulkan and NVIDIA's desktop OpenGL on Metal (`dm/gfx/nvn`, `dm/gfx/vk`, `dm/gfx/gl`). Sixty-five games are playable, each tested on the Mac or the iPhone (a Mac gameplay test is enough to count); the [games table](dm/games/README.md#games) lists each with its build, SDK, engine and graphics API, and links its note, `dm/games//README.md`: what its profile needs and why, what was found on it, how to drive it on the Mac, what is open. [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and power. - Where to start: | Task | Read first | diff --git a/README.md b/README.md index f6da8c4..373d2b0 100644 --- a/README.md +++ b/README.md @@ -120,19 +120,20 @@ Under each game: the IPA's size, and "60 fps with 1.2.0" where the game runs at Pokémon Legends: Z-A
Pokémon Legends: Z-A
~6 GB Pokémon Sword
Pokémon Sword
~12.4 GB -Raging Loop
Raging Loop
~0.9 GB +Portal
Portal
~2.6 GB +Raging Loop
Raging Loop
~0.9 GB Rhythm Heaven Groove
Rhythm Heaven Groove
~3 GB ROMANCE OF THE THREE KINGDOMS 8 REMAKE
ROMANCE OF THE THREE KINGDOMS 8 REMAKE
~16.8 GB · controller only -Stardew Valley
Stardew Valley
~0.6 GB +Stardew Valley
Stardew Valley
~0.6 GB STEINS;GATE RE:BOOT
STEINS;GATE RE:BOOT
~6 GB Super Smash Bros. Ultimate
Super Smash Bros. Ultimate
~17.5 GB · offline -Taiko Risshiden V DX
Taiko Risshiden V DX
~3.3 GB · Japanese · played by touch +Taiko Risshiden V DX
Taiko Risshiden V DX
~3.3 GB · Japanese · played by touch Unicorn Overlord
Unicorn Overlord
~4 GB diff --git a/dm/games/01007BB017812000/README.md b/dm/games/01007BB017812000/README.md new file mode 100644 index 0000000..2f4d33f --- /dev/null +++ b/dm/games/01007BB017812000/README.md @@ -0,0 +1,52 @@ +# Portal (`01007BB017812000`) + +Update 1.0.3 (`v196608`) over the base game, SDK 13.3.0. Valve's Source engine as NVIDIA Lightspeed Studios ported it to the Switch: `main` is a 0.5 MB launcher with SDL, and the engine is 28 NRO modules (`nro/*.nro` in the RomFS, registered by `.nrr/launcher_main.nrr`) loaded with `nn::ro`. It renders with NVN, its D3D9 layer linked into `materialsystem.nro`. Modules `main`, `subsdk0` (GLSLC), `subsdk2` (NintendoSDK_movie) and `sdk`; `subsdk1` is NVIDIA's OpenGL driver and is never loaded. + +**Current:** plays on the Mac at 60 fps: the opening movie, the title menu (its 3D background map), New Game, test chamber 00 with its portals (the view through them included), the pause menu and a saved game, with sound; no red line. About 1.5 GB footprint in the chamber. The unsigned IPA is built (2,823,721,518 bytes, about 2.6 GB); not yet tested on an iPhone. + +## What it needs + +| Setting | Where | Why | +|---|---|---| +| `modules main subsdk0 subsdk2 sdk` | [`game.tsv`](game.tsv) | `main` imports from no subsdk; the NROs do: `video_nx.nro` plays the movies through the movie module and both it and `materialsystem.nro` call GLSLC ([Modules only the NROs import from](../../../docs/reference/services.md#modules-only-the-nros-import-from)) | +| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | `game.tsv` | no system resource, so no virtual address memory; the game sets a heap in the 4 GB layout. The rewritten modules fit the default 64 MB of used memory | +| `pad_layout full` | `game.tsv` | first-person: move with the left stick, look with the right | +| `shader_groups /game.zip/=game` | `game.tsv` | the shipped programs come from `game.zip`; the run-time captures go to `other` | + +## Rendering + +- **Resolved: the NSP script took it for an OpenGL game.** `main` imports 20 EGL functions and names `glShaderSource` (SDL's EGL backend and GL renderer), but draws nothing; `materialsystem.nro` and `video_nx.nro` import `nvnBootstrapLoader`. The first capture boot stopped with `[nvn-mtl] FATAL cannot read the shader map` from an NVN submit in `nro8` ([NVN in NROs under an SDL main that names GL](../../../docs/reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl)). +- About 140 draws a frame at the title and in chamber 00; GPU p50 about 6 ms on an M4 Pro, NVN submit and present under 3 ms. + +## Shaders + +- The D3D9 layer links each Source shader combination (`hl2/shaders/fxc/*.vcs`, D3D9 bytecode) into one program and keeps the GLSLC output of every program in the game's precache list (`portal/glbaseshaders.cfg`, 219 entries) in `nxcontent/nvsb/.nvsb`: a 0x60-byte `NVSB` header (version 0x65, the file's length at +8), then one GLSLC output holding a vertex and a pixel shader. The game compiles the rest with `glslcCompile`: within 30 s of boot, 10 outputs (the movie's and the engine's own). +- **Resolved:** the split found none of the 219: they are deflated members of `game.zip` ([GLSLC outputs in a zip archive, and those NROs compile at run time](../../../docs/reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time)). The map has 255 keys, 165 pixel and 90 vertex, 242 from the `.nvsb` files and 13 from the capture, with 229 vertex and pixel pairs. +- The game writes the programs it has linked to its save, `portal/glshaders/glshaders.cfg` (`-glshadersSaveFS` in its boot parameters): 219 entries after chamber 00, the precache list. More entries there mean programs compiled at run time on that route, which a fresh capture boot does not reach; on the iPhone each would give `[RED] shader … is not in the shader map`. + +## Files + +- The whole game is `game.zip` (2.7 GB, 41,416 members, Deflate or stored) beside the NROs; the Source file system reads it itself. The profile's `romfs_marker` is `game.zip`. +- The boot parameters are `nxcontent/rom_boot_params.txt` in the zip (`-game portal -glshadersSaveFS -nodev -nosteam +mat_queue_mode 2 +mat_picmip 0`); host arguments after `--` (a `+map` among them) change nothing. +- Only the merged update is registered. The base game's build has no profile row. + +## Movies + +- The opening movie (1280x720 H.264 with AAC) plays through the host's movie decoder from `video_nx.nro`, which shows the frames with NVN and plays the sound through its own `AudioOut`. + +## Mac routes + +- Run the command the NSP script prints for `--platform mac` (`DM_NVN_SHMAP` names this profile's `shader_map.tsv`), with `DM_CTL=` to drive it ([05-debugging.md](../../../docs/agents/05-debugging.md)). +- From a fresh save: the title menu at about 25 s; `A` (New Game), `A` (Chapter 1) loads test chamber 00 in about 15 s. The portal opens in the vault after GLaDOS's first lines (about 30 s); the right stick turns, the left stick walks through it. `Plus` opens the pause menu; `Down`, `A`, `A` saves (`portal/save/*.sav` in the save). + +## Found on this game + +- [Modules only the NROs import from](../../../docs/reference/services.md#modules-only-the-nros-import-from): the provisional profile adds the subsdks the NROs need. +- [NVN in NROs under an SDL main that names GL](../../../docs/reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl): the graphics API follows the NROs' imports. +- [GLSLC outputs in a zip archive, and those NROs compile at run time](../../../docs/reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time): the split reads zip members; the run-time capture also runs for NROs that call `glslcCompile`. +- [`ro::LookupModuleSymbol` for a name the module does not define](../../../docs/reference/services.md#rolookupmodulesymbol-for-a-name-the-module-does-not-define): the engine's `BuiltDebug` probe is answered as on Horizon, without a red line. + +## Open + +- iPhone test of the IPA. +- Later chambers (energy balls, turrets, water, the escape) are untested; watch `glshaders.cfg` and the log for programs outside the shipped 219. diff --git a/dm/games/01007BB017812000/bf_want.tsv b/dm/games/01007BB017812000/bf_want.tsv new file mode 100644 index 0000000..54caeb8 --- /dev/null +++ b/dm/games/01007BB017812000/bf_want.tsv @@ -0,0 +1 @@ +# generated diff --git a/dm/games/01007BB017812000/game.tsv b/dm/games/01007BB017812000/game.tsv new file mode 100644 index 0000000..1cb9136 --- /dev/null +++ b/dm/games/01007BB017812000/game.tsv @@ -0,0 +1,37 @@ +# Portal 1.0.3, SDK 13.3.0. Valve's Source engine (NVIDIA Lightspeed Studios' Switch port): 28 NRO modules, NVN, shaders as GLSLC outputs. +# Mac bring-up and validation: README.md beside this profile. + +# ---- Identity ---- +title_id 01007BB017812000 +program_id 01007BB017812000 + +# ---- App ---- +name Portal +ipa Portal.ipa +app_category public.app-category.games +pad_layout full + +# ---- Versions ---- +build v196608 1.0.3 main=8ec6b5335d7ca09e21c9b6989cfa23fe subsdk0=2f31216323724e1ff64f03eef5fe030d subsdk2=7e9f31f59e001cf5d9b546ffb233f480 sdk=2bf3519216f5b9d3e8ce26863c2e98b6 +package v196608 +shader_versions v196608 + +# ---- Files ---- +# main imports from none of the subsdks; the NROs import GLSLC (subsdk0: materialsystem.nro and video_nx.nro) and the movie library (subsdk2: video_nx.nro). +# subsdk1 is NVIDIA's OpenGL driver, never loaded +modules main subsdk0 subsdk2 sdk +romfs_marker game.zip +zip_store .nro:.zip:.metallib + +# ---- Memory ---- +# main sets a heap of at most the pool − 64 MB: a 3328 MB heap region +mem_reserve 0x100000000 +mem_alias 0x10000000 +mem_stack 0x10000000 +app_pool 0xCD500000 +# the NPDM declares 0 MB of system resource: IsVirtualAddressMemoryEnabled is false, as on Horizon +sys_resource 0x0 + +# ---- Shader split ---- +shader_groups /game.zip/=game +shader_note Portal shaders diff --git a/dm/games/01007BB017812000/pso_manifest.tsv b/dm/games/01007BB017812000/pso_manifest.tsv new file mode 100644 index 0000000..4620515 --- /dev/null +++ b/dm/games/01007BB017812000/pso_manifest.tsv @@ -0,0 +1,2 @@ +# Prewarm seed list (shipped in the app, next to shader_map.tsv). Rows: Sshader keystage / Rrender pipeline key (pso_skey hex) / Ccompute shader key. +# Provisional profile: empty until recorded on the Mac (DM_PSO_MANIFEST=off DM_PSO_RECORD). diff --git a/dm/games/01007BB017812000/shader_facts.tsv b/dm/games/01007BB017812000/shader_facts.tsv new file mode 100644 index 0000000..e665d36 --- /dev/null +++ b/dm/games/01007BB017812000/shader_facts.tsv @@ -0,0 +1 @@ +# Answer table for translation queries (generated by dm/gfx/tools/shader_split.py, do not edit): key fact JSON. This game has no PC version to pair with: only the thread group / shared memory of compute shaders (taken from the control section); other queries get the translator defaults diff --git a/dm/games/01007BB017812000/shader_map.tsv b/dm/games/01007BB017812000/shader_map.tsv new file mode 100644 index 0000000..157ca66 --- /dev/null +++ b/dm/games/01007BB017812000/shader_map.tsv @@ -0,0 +1,258 @@ +# kind: evidence Portal shaders (GLSLC in RomFS + main) → control section + code area keys (dm/gfx/tools/shader_split.py) +# column 5 records where each key first appears; this game has no PC version to pair with +package index stage dm_ctl_sha16 source +ps_game.bin 0 pixel 125514da6678b858 /game.zip/nxcontent/nvsb/1006143466.nvsb#GLSLC@0x60#var +vs_game.bin 0 vertex 22eb0bfcd9d1228a /game.zip/nxcontent/nvsb/1006143466.nvsb#GLSLC@0x60#var +vs_game.bin 1 vertex 64b47f72b72d3927 /game.zip/nxcontent/nvsb/1010343794.nvsb#GLSLC@0x60#var +ps_game.bin 1 pixel b22b24b3038b9d0c /game.zip/nxcontent/nvsb/1010343794.nvsb#GLSLC@0x60#var +vs_game.bin 2 vertex 17d9f2e860f8e2a5 /game.zip/nxcontent/nvsb/101163401.nvsb#GLSLC@0x60#var +ps_game.bin 2 pixel 7c7c7d32a06ab09c /game.zip/nxcontent/nvsb/101163401.nvsb#GLSLC@0x60#var +ps_game.bin 3 pixel 99aa47484f5af30c /game.zip/nxcontent/nvsb/1033516092.nvsb#GLSLC@0x60#var +vs_game.bin 3 vertex 9b19085e1ae9241e /game.zip/nxcontent/nvsb/1033516092.nvsb#GLSLC@0x60#var +vs_game.bin 4 vertex 0240ee3cfc1e35f2 /game.zip/nxcontent/nvsb/1039914026.nvsb#GLSLC@0x60#var +ps_game.bin 4 pixel fcf7fa623131c989 /game.zip/nxcontent/nvsb/1039914026.nvsb#GLSLC@0x60#var +ps_game.bin 5 pixel 921fb80b384ab537 /game.zip/nxcontent/nvsb/1044132038.nvsb#GLSLC@0x60#var +vs_game.bin 5 vertex f37dda4622020cfc /game.zip/nxcontent/nvsb/1044132038.nvsb#GLSLC@0x60#var +ps_game.bin 6 pixel 8bbb1dc10b37bc05 /game.zip/nxcontent/nvsb/1058274740.nvsb#GLSLC@0x60#var +vs_game.bin 6 vertex df3028266d9a6623 /game.zip/nxcontent/nvsb/1058274740.nvsb#GLSLC@0x60#var +ps_game.bin 7 pixel 120f0a976a6ca107 /game.zip/nxcontent/nvsb/1059071465.nvsb#GLSLC@0x60#var +vs_game.bin 7 vertex 4c4942c65f7c6532 /game.zip/nxcontent/nvsb/1059071465.nvsb#GLSLC@0x60#var +ps_game.bin 8 pixel 6a7f9ee0809b5334 /game.zip/nxcontent/nvsb/106531108.nvsb#GLSLC@0x60#var +vs_game.bin 8 vertex 6e54afd255e5d137 /game.zip/nxcontent/nvsb/106531108.nvsb#GLSLC@0x60#var +ps_game.bin 9 pixel c9276c56710f4233 /game.zip/nxcontent/nvsb/1070782498.nvsb#GLSLC@0x60#var +vs_game.bin 9 vertex f3a6513a949ba831 /game.zip/nxcontent/nvsb/1070782498.nvsb#GLSLC@0x60#var +ps_game.bin 10 pixel d3ccef3531f7f7ca /game.zip/nxcontent/nvsb/1072446736.nvsb#GLSLC@0x60#var +vs_game.bin 10 vertex dc9c688b97391b8e /game.zip/nxcontent/nvsb/1072446736.nvsb#GLSLC@0x60#var +vs_game.bin 11 vertex 08b91fd9a17e6f7c /game.zip/nxcontent/nvsb/109986375.nvsb#GLSLC@0x60#var +ps_game.bin 11 pixel 5aa93f9962c2b334 /game.zip/nxcontent/nvsb/109986375.nvsb#GLSLC@0x60#var +ps_game.bin 12 pixel 235dcef274b4241d /game.zip/nxcontent/nvsb/1104172461.nvsb#GLSLC@0x60#var +vs_game.bin 12 vertex c3a892980c9f1bc1 /game.zip/nxcontent/nvsb/1104172461.nvsb#GLSLC@0x60#var +ps_game.bin 13 pixel 78a6875febbcb08a /game.zip/nxcontent/nvsb/1112455901.nvsb#GLSLC@0x60#var +ps_game.bin 14 pixel fc840c3fd38ddeaf /game.zip/nxcontent/nvsb/1113526353.nvsb#GLSLC@0x60#var +vs_game.bin 13 vertex 15f7efa3c3d1cbfe /game.zip/nxcontent/nvsb/1121413061.nvsb#GLSLC@0x60#var +ps_game.bin 15 pixel 432321c03163b463 /game.zip/nxcontent/nvsb/1121413061.nvsb#GLSLC@0x60#var +ps_game.bin 16 pixel 0ea0e34795da2fc9 /game.zip/nxcontent/nvsb/1122469837.nvsb#GLSLC@0x60#var +vs_game.bin 14 vertex 1937761849fa62c5 /game.zip/nxcontent/nvsb/1127042807.nvsb#GLSLC@0x60#var +ps_game.bin 17 pixel f189ac57671cb167 /game.zip/nxcontent/nvsb/1127042807.nvsb#GLSLC@0x60#var +ps_game.bin 18 pixel e453c5167faa9012 /game.zip/nxcontent/nvsb/1134249875.nvsb#GLSLC@0x60#var +ps_game.bin 19 pixel 39baf2e91789d314 /game.zip/nxcontent/nvsb/1138337703.nvsb#GLSLC@0x60#var +vs_game.bin 15 vertex 64dc0a38de10b093 /game.zip/nxcontent/nvsb/1138337703.nvsb#GLSLC@0x60#var +vs_game.bin 16 vertex 6eb7c453d9d43f1a /game.zip/nxcontent/nvsb/1156613687.nvsb#GLSLC@0x60#var +ps_game.bin 20 pixel c0579baa232152ed /game.zip/nxcontent/nvsb/1156613687.nvsb#GLSLC@0x60#var +vs_game.bin 17 vertex 220019d2f2617b5b /game.zip/nxcontent/nvsb/1158978846.nvsb#GLSLC@0x60#var +ps_game.bin 21 pixel ce84ed175d35887d /game.zip/nxcontent/nvsb/1158978846.nvsb#GLSLC@0x60#var +ps_game.bin 22 pixel 5c5df1542b5bff8b /game.zip/nxcontent/nvsb/1176889401.nvsb#GLSLC@0x60#var +ps_game.bin 23 pixel 31e59687e92d4493 /game.zip/nxcontent/nvsb/1179368768.nvsb#GLSLC@0x60#var +vs_game.bin 18 vertex fd1e81f63ebb59b6 /game.zip/nxcontent/nvsb/1179368768.nvsb#GLSLC@0x60#var +ps_game.bin 24 pixel e69295f44bb2da6e /game.zip/nxcontent/nvsb/1186111763.nvsb#GLSLC@0x60#var +vs_game.bin 19 vertex f9f665e0884417d4 /game.zip/nxcontent/nvsb/1186111763.nvsb#GLSLC@0x60#var +vs_game.bin 20 vertex a14e721cbc57dbff /game.zip/nxcontent/nvsb/1212092448.nvsb#GLSLC@0x60#var +ps_game.bin 25 pixel eee64002261e2982 /game.zip/nxcontent/nvsb/1212092448.nvsb#GLSLC@0x60#var +ps_game.bin 26 pixel 4c505d7aa81ac07a /game.zip/nxcontent/nvsb/121709879.nvsb#GLSLC@0x60#var +ps_game.bin 27 pixel 6c8255959ab5796a /game.zip/nxcontent/nvsb/1218347587.nvsb#GLSLC@0x60#var +vs_game.bin 21 vertex 9b4ab611524d7406 /game.zip/nxcontent/nvsb/1218347587.nvsb#GLSLC@0x60#var +ps_game.bin 28 pixel 44b1330452231fa9 /game.zip/nxcontent/nvsb/1224245560.nvsb#GLSLC@0x60#var +vs_game.bin 22 vertex e19d5b5cd1e76f1d /game.zip/nxcontent/nvsb/1224245560.nvsb#GLSLC@0x60#var +vs_game.bin 23 vertex 6ae3eddfa6e6d35d /game.zip/nxcontent/nvsb/122467106.nvsb#GLSLC@0x60#var +ps_game.bin 29 pixel cb711d71f83d596d /game.zip/nxcontent/nvsb/122467106.nvsb#GLSLC@0x60#var +vs_game.bin 24 vertex 82d6dcb828e50229 /game.zip/nxcontent/nvsb/1227682125.nvsb#GLSLC@0x60#var +ps_game.bin 30 pixel ae84535932b4dcd1 /game.zip/nxcontent/nvsb/1227682125.nvsb#GLSLC@0x60#var +ps_game.bin 31 pixel 4385e34c64c07c1d /game.zip/nxcontent/nvsb/1232496238.nvsb#GLSLC@0x60#var +vs_game.bin 25 vertex 55e810a06cfe54c9 /game.zip/nxcontent/nvsb/1236251869.nvsb#GLSLC@0x60#var +ps_game.bin 32 pixel d6c6872f8d53e813 /game.zip/nxcontent/nvsb/1236251869.nvsb#GLSLC@0x60#var +ps_game.bin 33 pixel 991b15d95129104d /game.zip/nxcontent/nvsb/1246009064.nvsb#GLSLC@0x60#var +vs_game.bin 26 vertex 7931c599b8d30c1e /game.zip/nxcontent/nvsb/1260989403.nvsb#GLSLC@0x60#var +ps_game.bin 34 pixel 8b667d60c1621df4 /game.zip/nxcontent/nvsb/1260989403.nvsb#GLSLC@0x60#var +ps_game.bin 35 pixel 2cecc0c2d3bc8fb8 /game.zip/nxcontent/nvsb/1271581352.nvsb#GLSLC@0x60#var +vs_game.bin 27 vertex dd6fd1212d52a7bd /game.zip/nxcontent/nvsb/1271581352.nvsb#GLSLC@0x60#var +vs_game.bin 28 vertex 098fe645af1bc986 /game.zip/nxcontent/nvsb/1273760642.nvsb#GLSLC@0x60#var +ps_game.bin 36 pixel 34619f350df1d911 /game.zip/nxcontent/nvsb/1273760642.nvsb#GLSLC@0x60#var +ps_game.bin 37 pixel b41301328a3f8c28 /game.zip/nxcontent/nvsb/1281255338.nvsb#GLSLC@0x60#var +ps_game.bin 38 pixel ec0de43959c4c3d1 /game.zip/nxcontent/nvsb/1296462734.nvsb#GLSLC@0x60#var +ps_game.bin 39 pixel ce7143191485a7b0 /game.zip/nxcontent/nvsb/1296726989.nvsb#GLSLC@0x60#var +ps_game.bin 40 pixel b75b2d5db59429d1 /game.zip/nxcontent/nvsb/1300570570.nvsb#GLSLC@0x60#var +vs_game.bin 29 vertex 2c2ea14f8cdeaf49 /game.zip/nxcontent/nvsb/130100692.nvsb#GLSLC@0x60#var +ps_game.bin 41 pixel e73df20777fde6fd /game.zip/nxcontent/nvsb/130100692.nvsb#GLSLC@0x60#var +ps_game.bin 42 pixel 2d7cc82b0233a2ba /game.zip/nxcontent/nvsb/130708013.nvsb#GLSLC@0x60#var +vs_game.bin 30 vertex ce78e77ecb956ccb /game.zip/nxcontent/nvsb/130708013.nvsb#GLSLC@0x60#var +ps_game.bin 43 pixel a627e6f4c4137eec /game.zip/nxcontent/nvsb/1307602202.nvsb#GLSLC@0x60#var +ps_game.bin 44 pixel ec2ba5e99e4b2b73 /game.zip/nxcontent/nvsb/1322805238.nvsb#GLSLC@0x60#var +ps_game.bin 45 pixel 1a7ec2fdca342c58 /game.zip/nxcontent/nvsb/1323899066.nvsb#GLSLC@0x60#var +vs_game.bin 31 vertex ed8b93820ce36531 /game.zip/nxcontent/nvsb/1323899066.nvsb#GLSLC@0x60#var +ps_game.bin 46 pixel d0d674eb2c33f483 /game.zip/nxcontent/nvsb/134129515.nvsb#GLSLC@0x60#var +vs_game.bin 32 vertex 2b8af06ab22a32ec /game.zip/nxcontent/nvsb/1342895557.nvsb#GLSLC@0x60#var +ps_game.bin 47 pixel a939f620ce3cae1b /game.zip/nxcontent/nvsb/1342895557.nvsb#GLSLC@0x60#var +ps_game.bin 48 pixel b64f68523fc22633 /game.zip/nxcontent/nvsb/1375227838.nvsb#GLSLC@0x60#var +vs_game.bin 33 vertex e897f9e7b7e7abe1 /game.zip/nxcontent/nvsb/1375227838.nvsb#GLSLC@0x60#var +ps_game.bin 49 pixel 19c793ec1c918157 /game.zip/nxcontent/nvsb/1384086589.nvsb#GLSLC@0x60#var +ps_game.bin 50 pixel 03a090d0c141931f /game.zip/nxcontent/nvsb/1405799114.nvsb#GLSLC@0x60#var +ps_game.bin 51 pixel 0f156839a8257e94 /game.zip/nxcontent/nvsb/1408040754.nvsb#GLSLC@0x60#var +vs_game.bin 34 vertex 0fe802630f6ae611 /game.zip/nxcontent/nvsb/1409487133.nvsb#GLSLC@0x60#var +ps_game.bin 52 pixel cde80507748f5a63 /game.zip/nxcontent/nvsb/1409487133.nvsb#GLSLC@0x60#var +vs_game.bin 35 vertex 5b49a1a917bcf780 /game.zip/nxcontent/nvsb/141818572.nvsb#GLSLC@0x60#var +ps_game.bin 53 pixel d99b483a105b1cc1 /game.zip/nxcontent/nvsb/141818572.nvsb#GLSLC@0x60#var +vs_game.bin 36 vertex 71aaf555a1e3b18f /game.zip/nxcontent/nvsb/1424981946.nvsb#GLSLC@0x60#var +ps_game.bin 54 pixel f2a62bcd66cce52f /game.zip/nxcontent/nvsb/1424981946.nvsb#GLSLC@0x60#var +ps_game.bin 55 pixel 3f088c3551875e14 /game.zip/nxcontent/nvsb/1426380811.nvsb#GLSLC@0x60#var +ps_game.bin 56 pixel 2d116c497e9ce78a /game.zip/nxcontent/nvsb/1433953797.nvsb#GLSLC@0x60#var +vs_game.bin 37 vertex 47f7e99e85c0ddda /game.zip/nxcontent/nvsb/1433953797.nvsb#GLSLC@0x60#var +ps_game.bin 57 pixel 35e3e36d6f8bb1b5 /game.zip/nxcontent/nvsb/146048892.nvsb#GLSLC@0x60#var +ps_game.bin 58 pixel 6f34c9f005f22799 /game.zip/nxcontent/nvsb/1470967011.nvsb#GLSLC@0x60#var +vs_game.bin 38 vertex 17fdd14a505256a7 /game.zip/nxcontent/nvsb/1471683114.nvsb#GLSLC@0x60#var +vs_game.bin 39 vertex f9dc2debb451353d /game.zip/nxcontent/nvsb/1493701505.nvsb#GLSLC@0x60#var +ps_game.bin 59 pixel 0804a53aa42aa796 /game.zip/nxcontent/nvsb/1498732659.nvsb#GLSLC@0x60#var +vs_game.bin 40 vertex 603676c9302b8660 /game.zip/nxcontent/nvsb/1498732659.nvsb#GLSLC@0x60#var +ps_game.bin 60 pixel 827d5514d57b13b2 /game.zip/nxcontent/nvsb/1503210799.nvsb#GLSLC@0x60#var +vs_game.bin 41 vertex 5903d6c17975e5b7 /game.zip/nxcontent/nvsb/1516114854.nvsb#GLSLC@0x60#var +ps_game.bin 61 pixel bf73234069e015cc /game.zip/nxcontent/nvsb/1524623539.nvsb#GLSLC@0x60#var +ps_game.bin 62 pixel adbc6564e34e5b0e /game.zip/nxcontent/nvsb/1552135254.nvsb#GLSLC@0x60#var +ps_game.bin 63 pixel acca61b3b9f9e261 /game.zip/nxcontent/nvsb/1559279768.nvsb#GLSLC@0x60#var +ps_game.bin 64 pixel 140a361ef37048e1 /game.zip/nxcontent/nvsb/1559618750.nvsb#GLSLC@0x60#var +vs_game.bin 42 vertex ef8a777c2f151751 /game.zip/nxcontent/nvsb/1559618750.nvsb#GLSLC@0x60#var +ps_game.bin 65 pixel d2d785c5eac5767f /game.zip/nxcontent/nvsb/1563425968.nvsb#GLSLC@0x60#var +vs_game.bin 43 vertex d4135c17b4835b51 /game.zip/nxcontent/nvsb/1563425968.nvsb#GLSLC@0x60#var +ps_game.bin 66 pixel 127c8193afe5da34 /game.zip/nxcontent/nvsb/1570033706.nvsb#GLSLC@0x60#var +ps_game.bin 67 pixel 4d97dcb634d624b0 /game.zip/nxcontent/nvsb/1590887012.nvsb#GLSLC@0x60#var +ps_game.bin 68 pixel 3cbe87361ecb3a6a /game.zip/nxcontent/nvsb/1592819690.nvsb#GLSLC@0x60#var +vs_game.bin 44 vertex b7e6be8139f55e7c /game.zip/nxcontent/nvsb/1592819690.nvsb#GLSLC@0x60#var +ps_game.bin 69 pixel ab17b21738878ae3 /game.zip/nxcontent/nvsb/161257982.nvsb#GLSLC@0x60#var +vs_game.bin 45 vertex 5561f4ee52b8be94 /game.zip/nxcontent/nvsb/1613295314.nvsb#GLSLC@0x60#var +ps_game.bin 70 pixel c6ae9c4fe005b98d /game.zip/nxcontent/nvsb/1613295314.nvsb#GLSLC@0x60#var +ps_game.bin 71 pixel 3103745341950b5e /game.zip/nxcontent/nvsb/1620148918.nvsb#GLSLC@0x60#var +vs_game.bin 46 vertex 8eeae34269830c21 /game.zip/nxcontent/nvsb/1631216478.nvsb#GLSLC@0x60#var +ps_game.bin 72 pixel 5069030326b3cec7 /game.zip/nxcontent/nvsb/1655032705.nvsb#GLSLC@0x60#var +vs_game.bin 47 vertex 77b5ea971d43d4e7 /game.zip/nxcontent/nvsb/1659476259.nvsb#GLSLC@0x60#var +vs_game.bin 48 vertex 9cf62937439a145a /game.zip/nxcontent/nvsb/167503881.nvsb#GLSLC@0x60#var +ps_game.bin 73 pixel 460bdaf26dc5d5de /game.zip/nxcontent/nvsb/1717637429.nvsb#GLSLC@0x60#var +vs_game.bin 49 vertex ac10fac9a065513b /game.zip/nxcontent/nvsb/1717637429.nvsb#GLSLC@0x60#var +ps_game.bin 74 pixel 6e05837c9919d4d5 /game.zip/nxcontent/nvsb/1745741.nvsb#GLSLC@0x60#var +vs_game.bin 50 vertex 8e6823fe228f42c7 /game.zip/nxcontent/nvsb/1745741.nvsb#GLSLC@0x60#var +ps_game.bin 75 pixel 7f33688bc96eb5e6 /game.zip/nxcontent/nvsb/1755612563.nvsb#GLSLC@0x60#var +ps_game.bin 76 pixel 98ba0f0dffbe06fb /game.zip/nxcontent/nvsb/1764038594.nvsb#GLSLC@0x60#var +ps_game.bin 77 pixel 02210f9e7fe6ad4c /game.zip/nxcontent/nvsb/1780532206.nvsb#GLSLC@0x60#var +vs_game.bin 51 vertex f67c93a064c9e2ff /game.zip/nxcontent/nvsb/17832264.nvsb#GLSLC@0x60#var +ps_game.bin 78 pixel 1ce3d86b64df835d /game.zip/nxcontent/nvsb/1789278367.nvsb#GLSLC@0x60#var +ps_game.bin 79 pixel 257ad8b3a826f85c /game.zip/nxcontent/nvsb/1798622250.nvsb#GLSLC@0x60#var +vs_game.bin 52 vertex 864e005418823018 /game.zip/nxcontent/nvsb/1798622250.nvsb#GLSLC@0x60#var +ps_game.bin 80 pixel a830eb7f17c96b0c /game.zip/nxcontent/nvsb/1805388630.nvsb#GLSLC@0x60#var +ps_game.bin 81 pixel 4aa15ca517fe177c /game.zip/nxcontent/nvsb/1816372284.nvsb#GLSLC@0x60#var +vs_game.bin 53 vertex 7876a08ed4433271 /game.zip/nxcontent/nvsb/1816372284.nvsb#GLSLC@0x60#var +ps_game.bin 82 pixel 739f1d303eeede4d /game.zip/nxcontent/nvsb/1821191205.nvsb#GLSLC@0x60#var +vs_game.bin 54 vertex aa2461d24e7a8851 /game.zip/nxcontent/nvsb/1821191205.nvsb#GLSLC@0x60#var +ps_game.bin 83 pixel 1c1a25b7bd1ffb0c /game.zip/nxcontent/nvsb/1836539311.nvsb#GLSLC@0x60#var +ps_game.bin 84 pixel 13adbf4ebad1e85f /game.zip/nxcontent/nvsb/1853280091.nvsb#GLSLC@0x60#var +vs_game.bin 55 vertex 530f785ceae5e1f2 /game.zip/nxcontent/nvsb/1863332167.nvsb#GLSLC@0x60#var +ps_game.bin 85 pixel 5f7ee404f03da9d5 /game.zip/nxcontent/nvsb/1863332167.nvsb#GLSLC@0x60#var +ps_game.bin 86 pixel 018aec9caf171747 /game.zip/nxcontent/nvsb/1869408490.nvsb#GLSLC@0x60#var +vs_game.bin 56 vertex 561a5a407a91c63e /game.zip/nxcontent/nvsb/1870088145.nvsb#GLSLC@0x60#var +ps_game.bin 87 pixel f89c7245b208efb1 /game.zip/nxcontent/nvsb/1870088145.nvsb#GLSLC@0x60#var +ps_game.bin 88 pixel 34637fa90566a012 /game.zip/nxcontent/nvsb/1874471462.nvsb#GLSLC@0x60#var +ps_game.bin 89 pixel 0755826d2bc85e5e /game.zip/nxcontent/nvsb/18791119.nvsb#GLSLC@0x60#var +vs_game.bin 57 vertex 67342b5be7f8d037 /game.zip/nxcontent/nvsb/1895959455.nvsb#GLSLC@0x60#var +ps_game.bin 90 pixel eb9388e0c6cc0978 /game.zip/nxcontent/nvsb/1895959455.nvsb#GLSLC@0x60#var +vs_game.bin 58 vertex 1e89027c28140037 /game.zip/nxcontent/nvsb/1900815595.nvsb#GLSLC@0x60#var +ps_game.bin 91 pixel ad8d97ad16639353 /game.zip/nxcontent/nvsb/1907849095.nvsb#GLSLC@0x60#var +ps_game.bin 92 pixel fac044187b750b53 /game.zip/nxcontent/nvsb/1928481773.nvsb#GLSLC@0x60#var +vs_game.bin 59 vertex 2addf1736ec8b694 /game.zip/nxcontent/nvsb/1954356698.nvsb#GLSLC@0x60#var +ps_game.bin 93 pixel 75e8486686c3fa41 /game.zip/nxcontent/nvsb/1954356698.nvsb#GLSLC@0x60#var +ps_game.bin 94 pixel 2d30b32a6bc599ee /game.zip/nxcontent/nvsb/1958778744.nvsb#GLSLC@0x60#var +ps_game.bin 95 pixel 9c89de94351e8ce4 /game.zip/nxcontent/nvsb/1972493923.nvsb#GLSLC@0x60#var +ps_game.bin 96 pixel a9ab86ce5a9e0084 /game.zip/nxcontent/nvsb/1982246115.nvsb#GLSLC@0x60#var +vs_game.bin 60 vertex aaa9fae84306cf73 /game.zip/nxcontent/nvsb/198379649.nvsb#GLSLC@0x60#var +ps_game.bin 97 pixel f35ed9b3e12d8df0 /game.zip/nxcontent/nvsb/198379649.nvsb#GLSLC@0x60#var +ps_game.bin 98 pixel 5655df85fdb4995c /game.zip/nxcontent/nvsb/2028069179.nvsb#GLSLC@0x60#var +ps_game.bin 99 pixel 543d0b5664802983 /game.zip/nxcontent/nvsb/2073046031.nvsb#GLSLC@0x60#var +ps_game.bin 100 pixel 611385f7b64b0554 /game.zip/nxcontent/nvsb/2093901329.nvsb#GLSLC@0x60#var +ps_game.bin 101 pixel cdf3e03c37046bcd /game.zip/nxcontent/nvsb/2107668394.nvsb#GLSLC@0x60#var +ps_game.bin 102 pixel ed068a953918fd2e /game.zip/nxcontent/nvsb/2119418873.nvsb#GLSLC@0x60#var +ps_game.bin 103 pixel 0d590a627839a1a1 /game.zip/nxcontent/nvsb/2120931521.nvsb#GLSLC@0x60#var +vs_game.bin 61 vertex 852fe378741e13e0 /game.zip/nxcontent/nvsb/2120931521.nvsb#GLSLC@0x60#var +ps_game.bin 104 pixel d7be276590968fb3 /game.zip/nxcontent/nvsb/2141879921.nvsb#GLSLC@0x60#var +vs_game.bin 62 vertex 41b384743b379ffc /game.zip/nxcontent/nvsb/220851893.nvsb#GLSLC@0x60#var +ps_game.bin 105 pixel 4ae7d858a1ce0bb6 /game.zip/nxcontent/nvsb/220851893.nvsb#GLSLC@0x60#var +vs_game.bin 63 vertex 9966c3d73fa41fc6 /game.zip/nxcontent/nvsb/247584683.nvsb#GLSLC@0x60#var +ps_game.bin 106 pixel fcb9bff3a1754048 /game.zip/nxcontent/nvsb/267742669.nvsb#GLSLC@0x60#var +ps_game.bin 107 pixel 89d9b8e71cc50e62 /game.zip/nxcontent/nvsb/278474355.nvsb#GLSLC@0x60#var +vs_game.bin 64 vertex cf41f4f0e9d550a8 /game.zip/nxcontent/nvsb/278474355.nvsb#GLSLC@0x60#var +ps_game.bin 108 pixel 50982dbd53d67424 /game.zip/nxcontent/nvsb/304289249.nvsb#GLSLC@0x60#var +vs_game.bin 65 vertex eee444ab1c8d1058 /game.zip/nxcontent/nvsb/304289249.nvsb#GLSLC@0x60#var +ps_game.bin 109 pixel fe2f8ee3742e4e13 /game.zip/nxcontent/nvsb/304482634.nvsb#GLSLC@0x60#var +vs_game.bin 66 vertex 378b66633d4f2ff9 /game.zip/nxcontent/nvsb/375914922.nvsb#GLSLC@0x60#var +ps_game.bin 110 pixel c76a368ffddc4c71 /game.zip/nxcontent/nvsb/375914922.nvsb#GLSLC@0x60#var +ps_game.bin 111 pixel 44df06c3f986986d /game.zip/nxcontent/nvsb/387789906.nvsb#GLSLC@0x60#var +vs_game.bin 67 vertex 71960d32d8e77601 /game.zip/nxcontent/nvsb/387789906.nvsb#GLSLC@0x60#var +ps_game.bin 112 pixel a40400c45a261601 /game.zip/nxcontent/nvsb/40007417.nvsb#GLSLC@0x60#var +ps_game.bin 113 pixel dcf9179af9d94365 /game.zip/nxcontent/nvsb/407424802.nvsb#GLSLC@0x60#var +vs_game.bin 68 vertex e50deab2af6277c6 /game.zip/nxcontent/nvsb/407424802.nvsb#GLSLC@0x60#var +ps_game.bin 114 pixel 5812b4a5572bfd4f /game.zip/nxcontent/nvsb/409312457.nvsb#GLSLC@0x60#var +ps_game.bin 115 pixel 199e9deea0791910 /game.zip/nxcontent/nvsb/434480402.nvsb#GLSLC@0x60#var +vs_game.bin 69 vertex c625ee5057b83a6e /game.zip/nxcontent/nvsb/434480402.nvsb#GLSLC@0x60#var +ps_game.bin 116 pixel e1d4e9caa1a2c27c /game.zip/nxcontent/nvsb/442310971.nvsb#GLSLC@0x60#var +ps_game.bin 117 pixel 73e697229d75fff5 /game.zip/nxcontent/nvsb/461195124.nvsb#GLSLC@0x60#var +vs_game.bin 70 vertex 8d1a6ed5ed0cd8b3 /game.zip/nxcontent/nvsb/461195124.nvsb#GLSLC@0x60#var +vs_game.bin 71 vertex 93b71d2999931f8b /game.zip/nxcontent/nvsb/482272727.nvsb#GLSLC@0x60#var +ps_game.bin 118 pixel ffda0f4a59db00c7 /game.zip/nxcontent/nvsb/498388205.nvsb#GLSLC@0x60#var +ps_game.bin 119 pixel 844e66ce8a093f31 /game.zip/nxcontent/nvsb/500652001.nvsb#GLSLC@0x60#var +ps_game.bin 120 pixel 557810e3c0241394 /game.zip/nxcontent/nvsb/503501776.nvsb#GLSLC@0x60#var +vs_game.bin 72 vertex 68d378bfe9669b84 /game.zip/nxcontent/nvsb/504892865.nvsb#GLSLC@0x60#var +ps_game.bin 121 pixel bae43c936113b364 /game.zip/nxcontent/nvsb/504892865.nvsb#GLSLC@0x60#var +ps_game.bin 122 pixel e1b7adb71904c320 /game.zip/nxcontent/nvsb/521814475.nvsb#GLSLC@0x60#var +vs_game.bin 73 vertex 145fca62b42aa364 /game.zip/nxcontent/nvsb/527443616.nvsb#GLSLC@0x60#var +ps_game.bin 123 pixel c1a64bac795c3fe8 /game.zip/nxcontent/nvsb/527443616.nvsb#GLSLC@0x60#var +vs_game.bin 74 vertex 74dbf128fe4eb7b9 /game.zip/nxcontent/nvsb/531681361.nvsb#GLSLC@0x60#var +ps_game.bin 124 pixel ad62d00c8d19b6e5 /game.zip/nxcontent/nvsb/531681361.nvsb#GLSLC@0x60#var +ps_game.bin 125 pixel 912ea84d3cde9fa1 /game.zip/nxcontent/nvsb/567545403.nvsb#GLSLC@0x60#var +ps_game.bin 126 pixel 4071785723d70eda /game.zip/nxcontent/nvsb/569494234.nvsb#GLSLC@0x60#var +ps_game.bin 127 pixel 826d84bf0106f618 /game.zip/nxcontent/nvsb/600527269.nvsb#GLSLC@0x60#var +vs_game.bin 75 vertex a4efef32dc784f2a /game.zip/nxcontent/nvsb/606447035.nvsb#GLSLC@0x60#var +ps_game.bin 128 pixel e505b6c6b31c35b4 /game.zip/nxcontent/nvsb/606447035.nvsb#GLSLC@0x60#var +ps_game.bin 129 pixel 3466d2c7fb252da9 /game.zip/nxcontent/nvsb/638428005.nvsb#GLSLC@0x60#var +vs_game.bin 76 vertex f64141b26f258051 /game.zip/nxcontent/nvsb/640315710.nvsb#GLSLC@0x60#var +ps_game.bin 130 pixel 800e7c4f63e66de3 /game.zip/nxcontent/nvsb/671384613.nvsb#GLSLC@0x60#var +ps_game.bin 131 pixel 1c51322649f4afb8 /game.zip/nxcontent/nvsb/677047702.nvsb#GLSLC@0x60#var +ps_game.bin 132 pixel 8b47f1e49f4fe97f /game.zip/nxcontent/nvsb/679512904.nvsb#GLSLC@0x60#var +ps_game.bin 133 pixel d3c7ed8ce47cb1f1 /game.zip/nxcontent/nvsb/689002777.nvsb#GLSLC@0x60#var +vs_game.bin 77 vertex eafff9c848e0ed33 /game.zip/nxcontent/nvsb/69649018.nvsb#GLSLC@0x60#var +ps_game.bin 134 pixel ed626950e6cc09dd /game.zip/nxcontent/nvsb/69649018.nvsb#GLSLC@0x60#var +vs_game.bin 78 vertex 692cc50abe7105df /game.zip/nxcontent/nvsb/701819308.nvsb#GLSLC@0x60#var +vs_game.bin 79 vertex ad0dba73e3521c4f /game.zip/nxcontent/nvsb/705551276.nvsb#GLSLC@0x60#var +vs_game.bin 80 vertex 0834ba1b19dddaa6 /game.zip/nxcontent/nvsb/709681957.nvsb#GLSLC@0x60#var +ps_game.bin 135 pixel c7d8fb5ccb420fec /game.zip/nxcontent/nvsb/709681957.nvsb#GLSLC@0x60#var +ps_game.bin 136 pixel 04357952b0c0d87b /game.zip/nxcontent/nvsb/714097666.nvsb#GLSLC@0x60#var +ps_game.bin 137 pixel 9d2987c1051bd452 /game.zip/nxcontent/nvsb/715711902.nvsb#GLSLC@0x60#var +ps_game.bin 138 pixel ee1322856b3e2eef /game.zip/nxcontent/nvsb/722804612.nvsb#GLSLC@0x60#var +ps_game.bin 139 pixel 20599900215db911 /game.zip/nxcontent/nvsb/724234349.nvsb#GLSLC@0x60#var +vs_game.bin 81 vertex d12cdec8456761f0 /game.zip/nxcontent/nvsb/724234349.nvsb#GLSLC@0x60#var +vs_game.bin 82 vertex 7a895bba6277cb3a /game.zip/nxcontent/nvsb/724284755.nvsb#GLSLC@0x60#var +ps_game.bin 140 pixel 4bc885832a18a327 /game.zip/nxcontent/nvsb/729449742.nvsb#GLSLC@0x60#var +vs_game.bin 83 vertex 4e5c2c26c2d5bb34 /game.zip/nxcontent/nvsb/729449742.nvsb#GLSLC@0x60#var +ps_game.bin 141 pixel dc8db55426582cd7 /game.zip/nxcontent/nvsb/737508209.nvsb#GLSLC@0x60#var +ps_game.bin 142 pixel 2ab95b13cd096921 /game.zip/nxcontent/nvsb/738207507.nvsb#GLSLC@0x60#var +ps_game.bin 143 pixel e9cf6ea5e928b306 /game.zip/nxcontent/nvsb/845921421.nvsb#GLSLC@0x60#var +ps_game.bin 144 pixel 1d8653b8824a528d /game.zip/nxcontent/nvsb/846435117.nvsb#GLSLC@0x60#var +vs_game.bin 84 vertex 8641dc25284850be /game.zip/nxcontent/nvsb/846435117.nvsb#GLSLC@0x60#var +ps_game.bin 145 pixel 5ce263b79d071865 /game.zip/nxcontent/nvsb/847560579.nvsb#GLSLC@0x60#var +ps_game.bin 146 pixel cb8e93a74d272f3a /game.zip/nxcontent/nvsb/853867118.nvsb#GLSLC@0x60#var +ps_game.bin 147 pixel cd624981849cdad4 /game.zip/nxcontent/nvsb/880357657.nvsb#GLSLC@0x60#var +ps_game.bin 148 pixel 15f50a859b2116d4 /game.zip/nxcontent/nvsb/931054901.nvsb#GLSLC@0x60#var +ps_game.bin 149 pixel a5b11ef684da02ad /game.zip/nxcontent/nvsb/931143399.nvsb#GLSLC@0x60#var +vs_game.bin 85 vertex 3bcda7edf3254840 /game.zip/nxcontent/nvsb/939889222.nvsb#GLSLC@0x60#var +ps_game.bin 150 pixel efa738504e37e486 /game.zip/nxcontent/nvsb/939889222.nvsb#GLSLC@0x60#var +ps_game.bin 151 pixel 4d037aa1c724e9f0 /game.zip/nxcontent/nvsb/947159063.nvsb#GLSLC@0x60#var +vs_game.bin 86 vertex 92059018c9c06875 /game.zip/nxcontent/nvsb/947159063.nvsb#GLSLC@0x60#var +ps_game.bin 152 pixel 70589daf1c863703 /game.zip/nxcontent/nvsb/98083864.nvsb#GLSLC@0x60#var +ps_game.bin 153 pixel 048a4a4a753e5076 /game.zip/nxcontent/nvsb/988541672.nvsb#GLSLC@0x60#var +ps_game.bin 154 pixel 05d01429bfeb63df /game.zip/nxcontent/nvsb/989117612.nvsb#GLSLC@0x60#var +ps_other.bin 0 pixel d4c2351bc7c67576 /glslc_capture/1aff81dac9e05541.glslc#GLSLC@0x0#var +ps_other.bin 1 pixel 0cc5c6c1aa721048 /glslc_capture/2f09b055791ff1b1.glslc#GLSLC@0x0#var +vs_other.bin 0 vertex 6cb0028a6b7401c6 /glslc_capture/2f09b055791ff1b1.glslc#GLSLC@0x0#var +ps_other.bin 2 pixel b4de8e96999bc6a5 /glslc_capture/547ef86375c3a210.glslc#GLSLC@0x0#var +ps_other.bin 3 pixel 5295c34e7179442e /glslc_capture/66f9af325711b20a.glslc#GLSLC@0x0#var +vs_other.bin 1 vertex d3ff93e32f2f20dc /glslc_capture/66f9af325711b20a.glslc#GLSLC@0x0#var +ps_other.bin 4 pixel f22bdcee695d2c44 /glslc_capture/702acc26648e0805.glslc#GLSLC@0x0#var +ps_other.bin 5 pixel 9b50f1715fa0c3e8 /glslc_capture/73ab6333e91ffc82.glslc#GLSLC@0x0#var +ps_other.bin 6 pixel 1ffbb41ab2c5afb1 /glslc_capture/bd3c8633f57640bb.glslc#GLSLC@0x0#var +ps_other.bin 7 pixel ae25b90688edc871 /glslc_capture/d6bcf71d9f5be8dc.glslc#GLSLC@0x0#var +ps_other.bin 8 pixel 2a46639cf42dbba7 /glslc_capture/da9eb384b238fd55.glslc#GLSLC@0x0#var +vs_other.bin 2 vertex 79d859b20b8b60d7 /glslc_capture/efbae697500ffa02.glslc#GLSLC@0x0#var +ps_other.bin 9 pixel ab7f1002ad1561b1 /glslc_capture/efbae697500ffa02.glslc#GLSLC@0x0#var diff --git a/dm/games/01007BB017812000/shader_map.tsv.conflicts.tsv b/dm/games/01007BB017812000/shader_map.tsv.conflicts.tsv new file mode 100644 index 0000000..35c1b4d --- /dev/null +++ b/dm/games/01007BB017812000/shader_map.tsv.conflicts.tsv @@ -0,0 +1,2 @@ +# kind: evidence control-section key conflict rulings: empty +dm_ctl_sha16 chosen_pc class candidates records diff --git a/dm/games/01007BB017812000/shader_pairs.tsv b/dm/games/01007BB017812000/shader_pairs.tsv new file mode 100644 index 0000000..389b9b5 --- /dev/null +++ b/dm/games/01007BB017812000/shader_pairs.tsv @@ -0,0 +1,230 @@ +# kind: evidence vertex / pixel shader pairs within BNSH variations (generated by dm/gfx/tools/shader_split.py, do not edit): vertex key pixel key +0240ee3cfc1e35f2 fcf7fa623131c989 +0834ba1b19dddaa6 c7d8fb5ccb420fec +08b91fd9a17e6f7c 5aa93f9962c2b334 +08b91fd9a17e6f7c d3c7ed8ce47cb1f1 +098fe645af1bc986 34619f350df1d911 +098fe645af1bc986 99aa47484f5af30c +0fe802630f6ae611 cde80507748f5a63 +145fca62b42aa364 c1a64bac795c3fe8 +15f7efa3c3d1cbfe 19c793ec1c918157 +15f7efa3c3d1cbfe 31e59687e92d4493 +15f7efa3c3d1cbfe 432321c03163b463 +17d9f2e860f8e2a5 0755826d2bc85e5e +17d9f2e860f8e2a5 1c51322649f4afb8 +17d9f2e860f8e2a5 5ce263b79d071865 +17d9f2e860f8e2a5 7c7c7d32a06ab09c +17d9f2e860f8e2a5 adbc6564e34e5b0e +17fdd14a505256a7 99aa47484f5af30c +1937761849fa62c5 03a090d0c141931f +1937761849fa62c5 0f156839a8257e94 +1937761849fa62c5 127c8193afe5da34 +1937761849fa62c5 1ce3d86b64df835d +1937761849fa62c5 3f088c3551875e14 +1937761849fa62c5 4385e34c64c07c1d +1937761849fa62c5 4c505d7aa81ac07a +1937761849fa62c5 4d97dcb634d624b0 +1937761849fa62c5 5069030326b3cec7 +1937761849fa62c5 6f34c9f005f22799 +1937761849fa62c5 827d5514d57b13b2 +1937761849fa62c5 912ea84d3cde9fa1 +1937761849fa62c5 a40400c45a261601 +1937761849fa62c5 acca61b3b9f9e261 +1937761849fa62c5 ad8d97ad16639353 +1937761849fa62c5 b41301328a3f8c28 +1937761849fa62c5 ce7143191485a7b0 +1937761849fa62c5 e453c5167faa9012 +1937761849fa62c5 ec2ba5e99e4b2b73 +1937761849fa62c5 f189ac57671cb167 +1937761849fa62c5 fc840c3fd38ddeaf +1937761849fa62c5 fcb9bff3a1754048 +1937761849fa62c5 ffda0f4a59db00c7 +1e89027c28140037 99aa47484f5af30c +220019d2f2617b5b cd624981849cdad4 +220019d2f2617b5b ce84ed175d35887d +22eb0bfcd9d1228a 018aec9caf171747 +22eb0bfcd9d1228a 03a090d0c141931f +22eb0bfcd9d1228a 05d01429bfeb63df +22eb0bfcd9d1228a 0f156839a8257e94 +22eb0bfcd9d1228a 125514da6678b858 +22eb0bfcd9d1228a 127c8193afe5da34 +22eb0bfcd9d1228a 1ce3d86b64df835d +22eb0bfcd9d1228a 3f088c3551875e14 +22eb0bfcd9d1228a 4385e34c64c07c1d +22eb0bfcd9d1228a 4c505d7aa81ac07a +22eb0bfcd9d1228a 4d97dcb634d624b0 +22eb0bfcd9d1228a 5069030326b3cec7 +22eb0bfcd9d1228a 543d0b5664802983 +22eb0bfcd9d1228a 6f34c9f005f22799 +22eb0bfcd9d1228a 78a6875febbcb08a +22eb0bfcd9d1228a 827d5514d57b13b2 +22eb0bfcd9d1228a 912ea84d3cde9fa1 +22eb0bfcd9d1228a 991b15d95129104d +22eb0bfcd9d1228a a40400c45a261601 +22eb0bfcd9d1228a a627e6f4c4137eec +22eb0bfcd9d1228a acca61b3b9f9e261 +22eb0bfcd9d1228a ad8d97ad16639353 +22eb0bfcd9d1228a b41301328a3f8c28 +22eb0bfcd9d1228a ce7143191485a7b0 +22eb0bfcd9d1228a dc8db55426582cd7 +22eb0bfcd9d1228a e1d4e9caa1a2c27c +22eb0bfcd9d1228a e453c5167faa9012 +22eb0bfcd9d1228a ec2ba5e99e4b2b73 +22eb0bfcd9d1228a f189ac57671cb167 +22eb0bfcd9d1228a fc840c3fd38ddeaf +22eb0bfcd9d1228a fcb9bff3a1754048 +22eb0bfcd9d1228a ffda0f4a59db00c7 +2addf1736ec8b694 75e8486686c3fa41 +2b8af06ab22a32ec 34637fa90566a012 +2b8af06ab22a32ec 7f33688bc96eb5e6 +2b8af06ab22a32ec a939f620ce3cae1b +2c2ea14f8cdeaf49 e73df20777fde6fd +378b66633d4f2ff9 c76a368ffddc4c71 +3bcda7edf3254840 efa738504e37e486 +41b384743b379ffc 4ae7d858a1ce0bb6 +47f7e99e85c0ddda 2d116c497e9ce78a +4c4942c65f7c6532 120f0a976a6ca107 +4e5c2c26c2d5bb34 4bc885832a18a327 +530f785ceae5e1f2 5f7ee404f03da9d5 +530f785ceae5e1f2 9c89de94351e8ce4 +5561f4ee52b8be94 c6ae9c4fe005b98d +55e810a06cfe54c9 d6c6872f8d53e813 +561a5a407a91c63e 15f50a859b2116d4 +561a5a407a91c63e f89c7245b208efb1 +5903d6c17975e5b7 19c793ec1c918157 +5903d6c17975e5b7 31e59687e92d4493 +5903d6c17975e5b7 432321c03163b463 +5b49a1a917bcf780 d99b483a105b1cc1 +603676c9302b8660 0804a53aa42aa796 +64b47f72b72d3927 0ea0e34795da2fc9 +64b47f72b72d3927 b22b24b3038b9d0c +64b47f72b72d3927 b75b2d5db59429d1 +64dc0a38de10b093 39baf2e91789d314 +67342b5be7f8d037 a5b11ef684da02ad +67342b5be7f8d037 eb9388e0c6cc0978 +68d378bfe9669b84 bae43c936113b364 +692cc50abe7105df c0579baa232152ed +6ae3eddfa6e6d35d 844e66ce8a093f31 +6ae3eddfa6e6d35d c0579baa232152ed +6ae3eddfa6e6d35d cb711d71f83d596d +6cb0028a6b7401c6 0cc5c6c1aa721048 +6cb0028a6b7401c6 d4c2351bc7c67576 +6e54afd255e5d137 5c5df1542b5bff8b +6e54afd255e5d137 6a7f9ee0809b5334 +6e54afd255e5d137 8bbb1dc10b37bc05 +6e54afd255e5d137 d0d674eb2c33f483 +6e54afd255e5d137 ed068a953918fd2e +6eb7c453d9d43f1a 99aa47484f5af30c +6eb7c453d9d43f1a c0579baa232152ed +71960d32d8e77601 44df06c3f986986d +71aaf555a1e3b18f ee1322856b3e2eef +71aaf555a1e3b18f f2a62bcd66cce52f +74dbf128fe4eb7b9 ad62d00c8d19b6e5 +77b5ea971d43d4e7 34619f350df1d911 +77b5ea971d43d4e7 99aa47484f5af30c +7876a08ed4433271 2d7cc82b0233a2ba +7876a08ed4433271 4aa15ca517fe177c +7876a08ed4433271 cb8e93a74d272f3a +7931c599b8d30c1e 8b667d60c1621df4 +79d859b20b8b60d7 1ffbb41ab2c5afb1 +79d859b20b8b60d7 2a46639cf42dbba7 +79d859b20b8b60d7 9b50f1715fa0c3e8 +79d859b20b8b60d7 ab7f1002ad1561b1 +79d859b20b8b60d7 ae25b90688edc871 +79d859b20b8b60d7 b4de8e96999bc6a5 +79d859b20b8b60d7 f22bdcee695d2c44 +7a895bba6277cb3a e73df20777fde6fd +82d6dcb828e50229 ae84535932b4dcd1 +852fe378741e13e0 0d590a627839a1a1 +8641dc25284850be 1d8653b8824a528d +864e005418823018 257ad8b3a826f85c +8d1a6ed5ed0cd8b3 73e697229d75fff5 +8e6823fe228f42c7 6e05837c9919d4d5 +8eeae34269830c21 19c793ec1c918157 +8eeae34269830c21 31e59687e92d4493 +8eeae34269830c21 432321c03163b463 +92059018c9c06875 4d037aa1c724e9f0 +93b71d2999931f8b 34619f350df1d911 +93b71d2999931f8b 99aa47484f5af30c +9966c3d73fa41fc6 0804a53aa42aa796 +9b19085e1ae9241e 34619f350df1d911 +9b19085e1ae9241e 99aa47484f5af30c +9b4ab611524d7406 6c8255959ab5796a +9b4ab611524d7406 9d2987c1051bd452 +9cf62937439a145a 19c793ec1c918157 +9cf62937439a145a ec0de43959c4c3d1 +a14e721cbc57dbff ec0de43959c4c3d1 +a14e721cbc57dbff eee64002261e2982 +a4efef32dc784f2a e505b6c6b31c35b4 +aa2461d24e7a8851 04357952b0c0d87b +aa2461d24e7a8851 611385f7b64b0554 +aa2461d24e7a8851 739f1d303eeede4d +aa2461d24e7a8851 8b47f1e49f4fe97f +aaa9fae84306cf73 f35ed9b3e12d8df0 +ac10fac9a065513b 460bdaf26dc5d5de +ac10fac9a065513b e9cf6ea5e928b306 +ad0dba73e3521c4f 44df06c3f986986d +b7e6be8139f55e7c 13adbf4ebad1e85f +b7e6be8139f55e7c 3cbe87361ecb3a6a +b7e6be8139f55e7c 4071785723d70eda +b7e6be8139f55e7c 98ba0f0dffbe06fb +b7e6be8139f55e7c a830eb7f17c96b0c +c3a892980c9f1bc1 235dcef274b4241d +c3a892980c9f1bc1 2ab95b13cd096921 +c3a892980c9f1bc1 3466d2c7fb252da9 +c3a892980c9f1bc1 ab17b21738878ae3 +c625ee5057b83a6e 199e9deea0791910 +ce78e77ecb956ccb 2d7cc82b0233a2ba +cf41f4f0e9d550a8 89d9b8e71cc50e62 +d12cdec8456761f0 20599900215db911 +d3ff93e32f2f20dc 5295c34e7179442e +d4135c17b4835b51 d2d785c5eac5767f +dc9c688b97391b8e 800e7c4f63e66de3 +dc9c688b97391b8e d3ccef3531f7f7ca +dd6fd1212d52a7bd 2cecc0c2d3bc8fb8 +dd6fd1212d52a7bd 35e3e36d6f8bb1b5 +dd6fd1212d52a7bd bf73234069e015cc +dd6fd1212d52a7bd d6c6872f8d53e813 +df3028266d9a6623 2d30b32a6bc599ee +df3028266d9a6623 5c5df1542b5bff8b +df3028266d9a6623 6a7f9ee0809b5334 +df3028266d9a6623 8bbb1dc10b37bc05 +df3028266d9a6623 d0d674eb2c33f483 +df3028266d9a6623 ed068a953918fd2e +e19d5b5cd1e76f1d 120f0a976a6ca107 +e19d5b5cd1e76f1d 3103745341950b5e +e19d5b5cd1e76f1d 44b1330452231fa9 +e19d5b5cd1e76f1d 826d84bf0106f618 +e19d5b5cd1e76f1d cdf3e03c37046bcd +e50deab2af6277c6 dcf9179af9d94365 +e897f9e7b7e7abe1 b64f68523fc22633 +eafff9c848e0ed33 ed626950e6cc09dd +ed8b93820ce36531 1a7ec2fdca342c58 +ed8b93820ce36531 5812b4a5572bfd4f +ed8b93820ce36531 fe2f8ee3742e4e13 +eee444ab1c8d1058 50982dbd53d67424 +eee444ab1c8d1058 70589daf1c863703 +ef8a777c2f151751 02210f9e7fe6ad4c +ef8a777c2f151751 048a4a4a753e5076 +ef8a777c2f151751 140a361ef37048e1 +ef8a777c2f151751 1c1a25b7bd1ffb0c +ef8a777c2f151751 a9ab86ce5a9e0084 +ef8a777c2f151751 d7be276590968fb3 +ef8a777c2f151751 e1b7adb71904c320 +ef8a777c2f151751 fac044187b750b53 +f37dda4622020cfc 921fb80b384ab537 +f3a6513a949ba831 557810e3c0241394 +f3a6513a949ba831 c9276c56710f4233 +f64141b26f258051 34619f350df1d911 +f67c93a064c9e2ff 2cecc0c2d3bc8fb8 +f67c93a064c9e2ff 35e3e36d6f8bb1b5 +f67c93a064c9e2ff bf73234069e015cc +f67c93a064c9e2ff d6c6872f8d53e813 +f9dc2debb451353d c0579baa232152ed +f9f665e0884417d4 39baf2e91789d314 +f9f665e0884417d4 e69295f44bb2da6e +fd1e81f63ebb59b6 19c793ec1c918157 +fd1e81f63ebb59b6 31e59687e92d4493 +fd1e81f63ebb59b6 432321c03163b463 +fd1e81f63ebb59b6 5655df85fdb4995c +fd1e81f63ebb59b6 ec0de43959c4c3d1 diff --git a/dm/games/README.md b/dm/games/README.md index 05fb4ed..6ab8539 100644 --- a/dm/games/README.md +++ b/dm/games/README.md @@ -85,6 +85,7 @@ One row per profile; the notes are each profile's `README.md`. | [Pikmin 4](0100B7C00933A000/README.md) | `0100B7C00933A000` | 1.1.1 | 10.4.8 | Unreal Engine 4.26.2 | NVN | | [Pokémon Legends: Z-A](0100F43008C44000/README.md) | `0100F43008C44000` | 2.0.2 | 19.3.11 | Game Freak's Trinity engine | NVN | | [Pokémon Sword](0100ABF008968000/README.md) | `0100ABF008968000` | 1.3.2 | 7.3.2 | Game Freak's engine (before Trinity) | NVN | +| [Portal](01007BB017812000/README.md) | `01007BB017812000` | 1.0.3 | 13.3.0 | Valve's Source engine (28 NROs) | NVN | | [Raging Loop](010020400E1C2000/README.md) | `010020400E1C2000` | 1.0.0 (base) | 7.3.2 | Unity 2018.3.10f1 IL2CPP | NVN | | [Rhythm Heaven Groove](0100D9F01D474000/README.md) | `0100D9F01D474000` | 1.0.0 (base) | 21.4.0 | Nintendo's own 2D engine | NVN | | [ROMANCE OF THE THREE KINGDOMS 8 REMAKE](0100C2A01A730000/README.md) | `0100C2A01A730000` | 1.1.3 | 17.5.3 | Koei Tecmo's KTGL | NVN | @@ -161,7 +162,7 @@ These are not profile keys: the tools read them from the game files each time, b 1. **Survey.** Run `scripts/build_ipa_from_nsp.py` on the game's NSP folder. Without a profile it extracts only the code modules and a RomFS list, then writes `/_survey/survey.md`. The survey covers modules and build IDs, SDK, engine and graphics API markers, RomFS containers, the rewrite-site census, and imports compared with the host hooks. Unless told `--survey-only`, the script then writes a **provisional profile** to `<NSP folder>/<title ID>_build/profile/<title ID>/` (outside the repository) and carries on to a best-effort IPA. `dm/games/profile_guess.py` fills its `game.tsv` from the game files, each key under the reason for it (the survey lists them too): - identity and name, the build row it found, the largest RomFS file as `romfs_marker`, big file types in `zip_store`, `shader_groups` by RomFS top-level folder; - - `modules`: a subsdk is loaded only when `main` imports something from it that the host does not supply itself. Under the Vulkan layer the Vulkan loader and GLSLC are dropped; movie modules and a GLSLC that `main` calls stay. NVIDIA's OpenGL and Vulkan drivers are never loaded: they program the GPU through NvRm, which the host does not have (Absolum imports EGL for SDL3's GL support and renders with NVN); + - `modules`: a subsdk is loaded only when `main` imports something from it that the host does not supply itself, or, once the rewrite stage has collected the game's NROs, when one of them does (`nro_modules`: Portal's Source engine NROs use the movie module and GLSLC, which its `main` does not). Under the Vulkan layer the Vulkan loader and GLSLC are dropped; movie modules and a GLSLC that `main` calls stay. NVIDIA's OpenGL and Vulkan drivers are never loaded: they program the GPU through NvRm, which the host does not have (Absolum imports EGL for SDL3's GL support and renders with NVN); - the memory partition. Without a system resource in the NPDM: the 4 GB layout, its heap region the pool − 64 MB − the bss of giant modules (Metroid Dread: 0x90000000). When `main` never sets a heap: a 1 GB block (Super Mario Galaxy). With a system resource: Engage's 5 GB block with the hardware pool, which the heap check corrects when the game sets a heap (without one the game maps everything in the alias region itself: Hollow Knight: Silksong, Absolum, Pokémon Legends: Z-A); - Engine presets: Hagi (`hagi.config`) gets `op_mode handheld` and `pad_layout full`. diff --git a/dm/games/profile_guess.py b/dm/games/profile_guess.py index 35c9dc9..faac15d 100644 --- a/dm/games/profile_guess.py +++ b/dm/games/profile_guess.py @@ -130,9 +130,8 @@ def guess(F, hooks=None): # Code modules: a subsdk main takes nothing from (nothing the host does not supply itself) is not loaded: NVIDIA's OpenGL / Vulkan drivers and the Vulkan loader under a Vulkan game # (the host's Vulkan layer answers main's vk* calls, and a loaded driver would take them first), GLSLC and nn::pcm under Super Mario Galaxy. The movie modules stay. # NVIDIA's OpenGL and Vulkan drivers are never loaded: they program the GPU through NvRm, which the host does not have. Absolum (FNA3D on NVN) imports EGL for SDL3's GL support, never used - order = lambda m: (m != 'main', m == 'sdk', int(m[6:]) if m.startswith('subsdk') and m[6:].isdigit() else 0) keep, drop = [], [] - for name in sorted((m for m in mods if m != 'rtld'), key=order): + for name in sorted((m for m in mods if m != 'rtld'), key=load_order): if name in ('main', 'sdk') or ((imp & mods[name]['exports']) - hooks and mods[name]['kind'] not in GPU_DRIVERS): keep.append(name) else: drop.append(f'{name} ({mods[name]["kind"]})') G['modules'] = (' '.join(keep), f'not loaded, main takes nothing from them the host does not supply: {", ".join(drop)}' if drop else 'every module main imports from') @@ -169,8 +168,36 @@ HEAP_API = '_ZN2nn2os17SetMemoryHeapSizeEm' GPU_DRIVERS = {'NintendoSDK_OpenGL', 'NintendoSDK_Vulkan'} +def load_order(m): # the modules' load order on Horizon: main, the subsdks by number, sdk + return (m != 'main', m == 'sdk', int(m[6:]) if m.startswith('subsdk') and m[6:].isdigit() else 0) + + +def nro_modules(exefs, nros, modules, hooks=None): + """The subsdks a provisional profile leaves out (main takes nothing from them) that the NROs the game loads at run time import from, {module: kind}: guess() sees only main's + imports, and the NROs are in the RomFS, which the survey does not extract. Portal (Source engine, 28 NROs) plays its movies in video_nx.nro through NintendoSDK_movie (subsdk2) + and initializes GLSLC (subsdk0) from materialsystem.nro and video_nx.nro; left out, movie::Extractor's constructor was a stub and the first movie crashed. + A symbol a loaded module or another NRO defines, or the host supplies, needs nothing; NVIDIA's GPU drivers stay out as in guess()""" + exefs, hooks = Path(exefs), host_hooks() if hooks is None else hooks + want, have = set(), set() + for p in nros.values(): + for s, defined, bind in nso_syms.dynsyms(Path(p).read_bytes()): + if not defined: want.add(s) + elif bind in (1, 2): have.add(s) + exp = {} + for p in sorted(exefs.iterdir()): + with open(p, 'rb') as f: + if f.read(4) != b'NSO0': continue + img = nso(p)[0] + exp[p.name] = {'exports': {s for s, defined, bind in nso_syms.dynsyms(img) if defined and bind in (1, 2)}, + 'comps': {c.decode().rsplit('-', 1)[0] for c in re.findall(rb'NintendoSDK_(?!nnSdk)\w+-[0-9_]+', img)}} + for m in modules: have |= exp.get(m, {}).get('exports', set()) + want -= have | hooks + return {m: kind(e) for m, e in sorted(exp.items(), key=lambda x: load_order(x[0])) + if m not in modules and m != 'rtld' and want & e['exports'] and kind(e) not in GPU_DRIVERS} + + # ---- Shader sources: decided from the game files at every build, not kept in the profile (scripts/build_ipa_from_nsp.py; shader_split.py reads what those steps write) ---- -def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_packs=0, legacy_paks=0, gl_names=0, gl_src=0, gl_files=0, oodle_pkz=0): +def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_packs=0, legacy_paks=0, gl_names=0, gl_src=0, gl_files=0, oodle_pkz=0, nro_imports=frozenset()): """{name: (value, reason)}: graphics (vulkan when main imports Vulkan functions and not nvnBootstrapLoader; gl when it imports eglCreateWindowSurface and names glShaderSource, so it compiles GLSL through OpenGL (gl_names: how often main has that name; Absolum imports the same EGL functions for SDL3 but renders with NVN and names no GL function); else nvn; vkGetInstanceProcAddr alone is SDL's Vulkan loader, which Balatro's LÖVE links and never uses), vk_capture (a Vulkan game without SPIR-V files generates its shaders at run time), gl_capture (an OpenGL game with no @@ -181,14 +208,20 @@ def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_pa unreal_paks counts the paks with shader code archives, legacy_paks those with inline shaders (uepak.inline_shaders). Checked against every profile: Hades and Super Mario Galaxy Vulkan, Galaxy alone with no SPIR-V file, Three Houses alone compiling shaders (13 Sentinels holds 106 GLSL sources but does not import glslcCompile; Captain Toad: Treasure Tracker imports it with agl's GLSL sources, but no module defines it), Octopath Traveler 0 alone with version 11 Unreal paks, DRAGON QUEST XI S alone with legacy ones (OCTOPATH TRAVELER II's version 11 pak has inline shaders too), Pokémon Legends: Z-A alone with Trinity packs, Digimon Story Cyber Sleuth and Balatro alone with OpenGL (of the 23 games - extracted; Hades and both Galaxies import 99 or 100 Vulkan functions, Balatro only vkGetInstanceProcAddr), Balatro alone composing its GLSL""" + extracted; Hades and both Galaxies import 99 or 100 Vulkan functions, Balatro only vkGetInstanceProcAddr), Balatro alone composing its GLSL. + nro_imports: what the NROs the game loads at run time import. Portal's main is SDL with its EGL backend (it names glShaderSource for SDL's GL renderer) and renders nothing: its Source engine + NROs draw with NVN (materialsystem.nro and video_nx.nro import nvnBootstrapLoader) and call glslcCompile; the GLSLC outputs it ships (nxcontent/nvsb in game.zip) are read by the split""" vk = sum(1 for s in imports if re.match(r'vk[A-Z]', s) and s != 'vkGetInstanceProcAddr'); vulkan = bool(vk) and 'nvnBootstrapLoader' not in imports - gl = not vulkan and 'eglCreateWindowSurface' in imports and bool(gl_names) + nro_nvn = 'nvnBootstrapLoader' in nro_imports + gl = not vulkan and not nro_nvn and 'eglCreateWindowSurface' in imports and bool(gl_names) + nro_glslc = not vulkan and not gl and 'glslcCompile' in nro_imports and bool(glslc_mods) return {'graphics': ('vulkan', f'main imports {vk} Vulkan functions and not nvnBootstrapLoader: the host\'s Vulkan layer (dm/gfx/vk/hle_vk.m)') if vulkan else - ('gl', 'main imports EGL and names glShaderSource: it renders with OpenGL and compiles GLSL, the host\'s GL layer (dm/gfx/gl/hle_gl.m)') if gl else ('nvn', 'main imports nvnBootstrapLoader'), + ('gl', 'main imports EGL and names glShaderSource: it renders with OpenGL and compiles GLSL, the host\'s GL layer (dm/gfx/gl/hle_gl.m)') if gl else + ('nvn', 'the NROs the game loads import nvnBootstrapLoader' if nro_nvn and 'nvnBootstrapLoader' not in imports else 'main imports nvnBootstrapLoader'), 'vk_capture': (vulkan and not spirv, 'no SPIR-V file in the RomFS: the game generates its shaders at run time (Super Mario Galaxy), captured by booting it on the Mac'), 'gl_capture': (gl and not gl_src and not gl_files, 'no GLSL source in the RomFS or main: the game puts its shaders together at run time (LÖVE: Balatro), captured by booting it on the Mac'), - 'glslc_capture': (not vulkan and not gl and bool(glsl_src) and 'glslcCompile' in imports and bool(glslc_mods), + 'glslc_capture': (not vulkan and not gl and bool(glsl_src) and 'glslcCompile' in imports and bool(glslc_mods) or nro_glslc, + f'the NROs the game loads call glslcCompile in {", ".join(glslc_mods)}: shaders compiled at run time (Portal), captured by booting it on the Mac' if nro_glslc and not (glsl_src and 'glslcCompile' in imports) else f'main holds {glsl_src} GLSL sources and calls glslcCompile in {", ".join(glslc_mods) or "no module it loads"}: shaders compiled at run time (Fire Emblem: Three Houses), captured by booting it on the Mac'), 'ue_decode': (not vulkan and not gl and bool(unreal_paks), f'{unreal_paks} Unreal pak(s): the shaders are Oodle-compressed in their shader code archives (Octopath Traveler 0), decoded by booting the game on the Mac'), 'oodle_packs': (not vulkan and not gl and bool(trinity_packs or oodle_pkz), (f'{trinity_packs} Trinity pack file(s) (ONEPACK, arc/data.trpfs: Pokémon Legends: Z-A)' if trinity_packs else @@ -197,12 +230,25 @@ def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_pa 'version 11 without shader code archives, UE 4.27: OCTOPATH TRAVELER II, Zlib): the shader split reads them file by file')} +def romfs_nro_imports(romfs): # the names the loose NROs in a RomFS import (the modules a game loads at run time with nn::ro: Portal's 28 Source engine modules in nro/) + out = set() + for d, _, names in os.walk(romfs): + for n in names: + if n.endswith('.nro'): + b = Path(d, n).read_bytes() + if b[0x10:0x14] == b'NRO0': out |= {s for s, defined, _ in nso_syms.dynsyms(b) if not defined} + return out + + def build_sources(exefs, romfs, modules): """shader_sources from an extraction (its ExeFS and RomFS folders) and the modules the profile loads""" exefs = Path(exefs); main = nso(exefs / 'main')[0] imports = {s for s, defined, _ in nso_syms.dynsyms(main) if not defined}; glsl_src = main.count(b'#version '); gl_names = main.count(b'glShaderSource'); gl_src = len(re.findall(GL_SRC, main)) - glslc_mods = [m for m in modules if m != 'main' and glsl_src and 'glslcCompile' in imports and (exefs / m).exists() - and any(s == 'glslcCompile' and defined for s, defined, _ in nso_syms.dynsyms(nso(exefs / m)[0]))] + nro_imp = romfs_nro_imports(romfs) + defines = lambda m: (exefs / m).exists() and any(s == 'glslcCompile' and defined for s, defined, _ in nso_syms.dynsyms(nso(exefs / m)[0])) + glslc_mods = [m for m in modules if m != 'main' and glsl_src and 'glslcCompile' in imports and defines(m)] + if 'glslcCompile' in nro_imp: # an NRO compiles (Portal's materialsystem.nro): the GLSLC module is one nro_modules adds to a provisional profile, so any of the ExeFS's counts + glslc_mods = sorted({*glslc_mods, *(p.name for p in exefs.iterdir() if p.name.startswith('subsdk') and defines(p.name))}, key=load_order) spirv = 0 if any(re.match(r'vk[A-Z]', s) for s in imports): # only a Vulkan game's RomFS is read spirv = sum(1 for _, _, head in romfs_files(romfs) if head.startswith(bytes.fromhex('03022307'))) @@ -221,7 +267,7 @@ def build_sources(exefs, romfs, modules): h = f.read(0x20) if h[:4] == b'pkzl': cnt, eo, nl = struct.unpack_from('<III', h, 0x10); f.seek(eo + 0x20 * cnt); oodle_pkz += b'OodleKraken\0' in f.read(nl) - return shader_sources(imports, glsl_src, glslc_mods, spirv, len(uepak.unreal_paks(str(romfs), inline=False)), trinity, len(uepak.unreal_paks(str(romfs), inline=True)), gl_names, gl_src, gl_files, oodle_pkz) + return shader_sources(imports, glsl_src, glslc_mods, spirv, len(uepak.unreal_paks(str(romfs), inline=False)), trinity, len(uepak.unreal_paks(str(romfs), inline=True)), gl_names, gl_src, gl_files, oodle_pkz, nro_imp) def va_heap(sysres, imports): # virtual address memory (an NPDM system resource) and a heap: the alias / heap split is a starting point the heap check corrects (Wonder, Tears of the Kingdom; Engage's pool is its own measurement) @@ -296,6 +342,12 @@ def find_extraction(root, g): # (ExeFS, RomFS manifest or folder) for a profil return None +def loose_nros(root, g, ex): # {name: path} of the loose NROs in a profile's RomFS: the extraction's own folder, else an NSP script extraction of the packed version beside its survey + roms = [Path(ex[1])] if Path(ex[1]).is_dir() else [d / f'{g.id}_build/extract' / g.get('package', '') / 'RomFS' for d in sorted(Path(root).iterdir())] + rom = next((r for r in roms if r.is_dir()), None) + return {p.name: p for p in sorted(rom.rglob('*.nro')) if p.is_file()} if rom else {} + + def check(root): sys.path.insert(0, str(HERE)); import game hooks, bad = host_hooks(), 0 @@ -304,6 +356,9 @@ def check(root): ex = find_extraction(root, g) if not ex: print(f'{g.id} {g.get("name")}: no extraction under {root}'); continue F = facts_of(*ex); G = guess(F, hooks) + nros = loose_nros(root, g, ex) # what the NSP script adds once the rewrite stage has collected the NROs (a survey's RomFS manifest has none to read) + if nros and (add := nro_modules(ex[0], nros, G['modules'][0].split(), hooks)): + G['modules'] = (' '.join(sorted({*G['modules'][0].split(), *add}, key=load_order)), '') diff, unknown, later = [], [], [] for k in list(DEFAULTS) + ['modules', 'mem_reserve', 'mem_alias', 'mem_stack', 'app_pool']: want, got = g.get(k, DEFAULTS.get(k, '')), G.get(k, (DEFAULTS.get(k, ''),))[0] diff --git a/dm/gfx/tools/shader_split.py b/dm/gfx/tools/shader_split.py index 1b99a6a..74da417 100644 --- a/dm/gfx/tools/shader_split.py +++ b/dm/gfx/tools/shader_split.py @@ -471,6 +471,37 @@ def cpk_shaders(path, rel, ents, out): print(f'[RED] /{rel}/{e[0]}: {x}; not scanned', flush=True); continue scan_blob(d, f'/{rel}/{e[0]}', out) +ZIP = b'PK\x03\x04' +def zip_jobs(files, parts=64): + """Every member of every zip archive in ranges of about equal length (Portal: the whole game in game.zip, 41,416 members, 4.6 GB inflated, Deflate or stored; its GLSLC outputs are the + 221 nxcontent/nvsb/*.nvsb files, but no name is relied on). → ([(path, rel, (ZIP, members)), …], the zip paths). An archive zipfile cannot read is scanned as one file, as before""" + import zipfile + zs = [] + for p, r in files: + with open(p, 'rb') as f: h = f.read(4) + if h != ZIP: continue + try: + with zipfile.ZipFile(p) as z: zs.append((p, r, [(i.filename, i.file_size) for i in z.infolist() if not i.is_dir()])) + except (zipfile.BadZipFile, OSError, ValueError) as e: print(f'[RED] /{r}: the zip cannot be read ({e}); scanned as one file', flush=True) + jobs, step = [], sum(n for _, _, es in zs for _, n in es) / parts + for p, r, es in zs: + cur, size = [], 0 + for nm, n in es: + cur.append(nm); size += n + if size >= step: jobs.append((p, r, (ZIP, tuple(cur)))); cur, size = [], 0 + if cur: jobs.append((p, r, (ZIP, tuple(cur)))) + return jobs, {p for p, _, _ in zs} + +def zip_shaders(path, rel, names, out): + """A range of zip members (zip_jobs), each inflated and scanned like a loose file: scan_blob, then whole GLSLC outputs (Portal's .nvsb files: a 0x60-byte 'NVSB' header, then one + GLSLC output of a vertex and a pixel shader)""" + import zipfile + with zipfile.ZipFile(path) as z: + for nm in names: + d = z.read(nm); n = len(out) + scan_blob(d, f'/{rel}/{nm}', out) + if len(out) == n and d[:4] != b'MSHD' and b'\x91\x68\x86\x19' in d: out += glslc_shaders(d, f'/{rel}/{nm}', need=False) + def pgdat_members(d, label): """A PlatinumGames DAT archive: 'DAT\\0', u32 file count, then the offsets of five tables: file offsets (u32 each), extensions (4 bytes each), names (u32 name length, then one name of that length each), file lengths (u32 each), a name hash map. Members may be DAT archives again. → [(name, data)], or None when the tables do not fit the data (another game's file that only starts @@ -1581,6 +1612,9 @@ def scan_file(args): if len(args) > 2 and head[:8] == ONEPACK: # a range of Trinity packs (trpfs_jobs) trinity_shaders(path, rel, args[2], found) return rel, uniq_keys(found), var_pairs(found), trees + if len(args) > 2 and head[:4] == ZIP and args[2][0] == ZIP: # members of a zip archive (zip_jobs) + zip_shaders(path, rel, args[2][1], found) + return rel, uniq_keys(found), var_pairs(found), trees if len(args) > 2 and args[2][0] == UE_LEGACY: # files of a legacy Unreal pak (ue_legacy_jobs) ue_legacy_shaders(path, rel, args[2][1], found) return rel, uniq_keys(found), var_pairs(found), trees @@ -1698,8 +1732,9 @@ def main(): arc_jobs, arcs = smash_arc_jobs(files) # Super Smash Bros. Ultimate's data.arc: its current files, in ranges pz_jobs, pkzs = pkz_jobs(files) # Bayonetta 3's PKZ archives, Oodle Kraken decoded by the game's own decoder in the Mac host ck_jobs, cpks = cpk_jobs(files) # Bayonetta 2's CRI CPK archives (CRILAYLA entries) - big += pak_jobs + tr_jobs + ue_jobs + bd_jobs + arc_jobs + pz_jobs + ck_jobs - files = [(p, r) for p, r in files if not linkdata_jobs(p, r) and p not in paks and p not in trp and p not in uep and p not in bdts and p not in arcs and p not in pkzs and p not in cpks] + zp_jobs, zips = zip_jobs(files) # Portal's game.zip (Deflate members) + big += pak_jobs + tr_jobs + ue_jobs + bd_jobs + arc_jobs + pz_jobs + ck_jobs + zp_jobs + files = [(p, r) for p, r in files if not linkdata_jobs(p, r) and p not in paks and p not in trp and p not in uep and p not in bdts and p not in arcs and p not in pkzs and p not in cpks and p not in zips] with cf.ProcessPoolExecutor(J, initializer=_pool_init, initargs=(common, {}, zdicts)) as ex: results += list(ex.map(scan_file, big)) + list(ex.map(scan_file, files, chunksize=16)) trees, defer = {}, [] @@ -1743,7 +1778,7 @@ def main(): for name, rr in recs.items(): with open(os.path.join(pkg, name), 'wb') as f: f.write(b''.join(rr)) with open(mapout, 'w') as f: - kinds = ' / '.join(k for k, hit in (('BNSH / BAHS', any('#BNSH@' in r[4] or '#BAHS@' in r[4] for r in rows)), ('Unity Shader', any('#var' in r[4] and '#BNSH@' not in r[4] and '#MSHD#' not in r[4] and '#SDF#' not in r[4] for r in rows)), + kinds = ' / '.join(k for k, hit in (('BNSH / BAHS', any('#BNSH@' in r[4] or '#BAHS@' in r[4] for r in rows)), ('Unity Shader', any('#var' in r[4] and '#BNSH@' not in r[4] and '#MSHD#' not in r[4] and '#SDF#' not in r[4] and '#GLSLC@' not in r[4] for r in rows)), ('MSHD', any('#MSHD#' in r[4] for r in rows)), ('GLSLC', any('#GLSLC@' in r[4] for r in rows)), ('RE Engine SDF', any('#SDF#' in r[4] for r in rows)), ('MT Framework SPK', any('#MTSPK@' in r[4] for r in rows)), ('MonoGame MGFX', any('#MGFX@' in r[4] for r in rows)), ('PlatinumGames shader objects', any('#PGSO' in r[4] for r in rows)), diff --git a/dm/host/hle_ro.c b/dm/host/hle_ro.c index 5d41444..d6cdfa0 100644 --- a/dm/host/hle_ro.c +++ b/dm/host/hle_ro.c @@ -33,7 +33,10 @@ static module_t *module_from(const ro_module *rm) { HLE_FORCE("_ZN2nn2ro18LookupModuleSymbolEPmPKNS0_6ModuleEPKc", "look up a definition by name in one loaded NRO", Result, h_ro_LookupModuleSymbol, uintptr_t *out, const void *mod, const char *name) { module_t *m = module_from(mod); void *p = m && name ? module_sym(m, name) : NULL; - if (!p) { static int red; if (!red++) LOG("[RED] ro::LookupModuleSymbol %s: %s", name ? name : "(null)", m ? "not defined in that module" : "not a loaded module"); return RO_FAIL; } + // A name the loaded module does not define fails on Horizon too (ldr:ro's not-found result), and games probe for one: Portal's Source engine asks each module for BuiltDebug, which + // only its debug builds define. Only a module the host does not know is wrong + if (!p && m) { static int n; if (n++ < 8) LOG("ro::LookupModuleSymbol %s: not defined in that module (failure, as on Horizon)", name ? name : "(null)"); return RO_FAIL; } + if (!p) { static int red; if (!red++) LOG("[RED] ro::LookupModuleSymbol %s: not a loaded module", name ? name : "(null)"); return RO_FAIL; } if (out) *out = (uintptr_t)p; return 0; } diff --git a/dm/tools/tests/test_nro_graphics.py b/dm/tools/tests/test_nro_graphics.py new file mode 100644 index 0000000..d19703b --- /dev/null +++ b/dm/tools/tests/test_nro_graphics.py @@ -0,0 +1,63 @@ +"""What a game's NROs decide (Portal: the Source engine in 28 NROs under an SDL main): the graphics API, the run-time GLSLC capture, and zip members for the shader split.""" +import os +from pathlib import Path +import shutil +import sys +import tempfile +import unittest +import zipfile + +REPO = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(REPO / 'dm/games')); import profile_guess as pg +_had = os.environ.get('DM_GAME'); os.environ['DM_GAME'] = '01007BB017812000' # shader_split loads a profile at import; other tests expect no default game +sys.path.insert(0, str(REPO / 'dm/gfx/tools')); import shader_split as ss +if _had is None: del os.environ['DM_GAME'] +else: os.environ['DM_GAME'] = _had + +SDL_MAIN = {'eglCreateWindowSurface', 'eglGetProcAddress'} # SDL's EGL backend; main names glShaderSource (gl_names) for SDL's GL renderer + + +class NroGraphics(unittest.TestCase): + def test_nvn_in_nros_beats_an_sdl_main(self): + s = pg.shader_sources(SDL_MAIN, 0, ['subsdk0'], 0, 0, gl_names=2, nro_imports={'nvnBootstrapLoader', 'glslcCompile'}) + self.assertEqual(s['graphics'][0], 'nvn') + self.assertTrue(s['glslc_capture'][0]) + self.assertFalse(s['gl_capture'][0]) + + def test_gl_without_nros_unchanged(self): # Digimon Story Cyber Sleuth: no NRO, GL through EGL + s = pg.shader_sources(SDL_MAIN | {'nvnBootstrapLoader'}, 0, [], 0, 0, gl_names=1, gl_files=1) + self.assertEqual(s['graphics'][0], 'gl') + self.assertFalse(s['glslc_capture'][0]) + + def test_nro_glslc_needs_a_module_that_defines_it(self): + s = pg.shader_sources(set(), 0, [], 0, 0, nro_imports={'nvnBootstrapLoader', 'glslcCompile'}) + self.assertEqual(s['graphics'][0], 'nvn') + self.assertFalse(s['glslc_capture'][0]) + + def test_load_order(self): + self.assertEqual(sorted(['sdk', 'subsdk2', 'main', 'subsdk0', 'subsdk10'], key=pg.load_order), ['main', 'subsdk0', 'subsdk2', 'subsdk10', 'sdk']) + + +class ZipMembers(unittest.TestCase): + def setUp(self): + self.tmp = Path(tempfile.mkdtemp()); self.addCleanup(shutil.rmtree, self.tmp) + + def test_members_in_ranges_and_not_scanned_whole(self): + z = self.tmp / 'game.zip' + with zipfile.ZipFile(z, 'w', zipfile.ZIP_DEFLATED) as f: + f.writestr('nxcontent/nvsb/', b'') + for i in range(10): f.writestr(f'nxcontent/nvsb/{i}.nvsb', bytes(100)) + (self.tmp / 'loose.bin').write_bytes(b'not a zip') + jobs, zips = ss.zip_jobs([(str(z), 'game.zip'), (str(self.tmp / 'loose.bin'), 'loose.bin')], parts=4) + self.assertEqual(zips, {str(z)}) + names = [n for _, _, (tag, ns) in jobs for n in ns] + self.assertTrue(all(tag == ss.ZIP for _, _, (tag, _) in jobs)) + self.assertEqual(sorted(names), sorted(f'nxcontent/nvsb/{i}.nvsb' for i in range(10))) # the directory entry is left out + self.assertGreater(len(jobs), 1) + out = [] + for p, r, (_, ns) in jobs: ss.zip_shaders(p, r, ns, out) + self.assertEqual(out, []) # zero-filled members hold no shader + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index ed76868..029c7de 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -153,6 +153,8 @@ One line per symptom in the [subsystem references](../reference/): what you see - the frame loop runs, but the game waits for code it loads itself (`ro::RegisterModuleInfo` / `ro::GetBufferSize` red lines; `main` imports 1,045 `lua2cpp::create_agent_*` functions, global, that no ExeFS module defines, bound to stubs at start). → [Loading NROs with `ro::LoadModule`](../reference/services.md#loading-nros-with-roloadmodule) - `[EOS] Failed to create platform` (the game plays offline); `refused …: an online SDK (Epic Online Services), not loaded`; `refused an NRR that registers only online SDKs (1 hashes)`. → [Online SDKs in NROs are never loaded](../reference/services.md#online-sdks-in-nros-are-never-loaded) - `main` imports EGL (SDL3's GL support) and `vkGetInstanceProcAddr` (its Vulkan support), but the game renders with NVN (`FNA3D_Driver_NVN.cpp`). → [GPU driver modules that are never loaded](../reference/services.md#gpu-driver-modules-that-are-never-loaded) +- `UNIMPL _ZN5movie12SetAllocatorEPFPvmmS0_EPFvS0_S0_EPFS0_S0_mS0_ES0_`, `UNIMPL glslcInitialize`, then `UNIMPL _ZN5movie9ExtractorC1ENS_13ContainerTypeE` and a crash reading address 0x18 in an NRO (`pc=nro18+0x97f4`) as the first movie starts. → [Modules only the NROs import from](../reference/services.md#modules-only-the-nros-import-from) +- `[RED] ro::LookupModuleSymbol BuiltDebug: not defined in that module` once at boot; nothing goes wrong after it. → [`ro::LookupModuleSymbol` for a name the module does not define](../reference/services.md#rolookupmodulesymbol-for-a-name-the-module-does-not-define) - harmless on the build it was written for, silent memory corruption on any other. → [Code inherited from another title at fixed addresses](../reference/services.md#code-inherited-from-another-title-at-fixed-addresses) ## Movies @@ -334,6 +336,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - GX points and lines drawn as one-pixel points and lines; a red line for the sampler LOD bias. → [Not translated in the Vulkan layer: geometry shaders and LOD bias](../reference/rendering.md#not-translated-in-the-vulkan-layer-geometry-shaders-and-lod-bias) - `UNIMPL eglGetDisplay`, then `Break` right after `nv::InitializeGraphics` and `vi::OpenDefaultDisplay` (`main+0x5ed9d0`). → [Games that render with desktop OpenGL through EGL](../reference/rendering.md#games-that-render-with-desktop-opengl-through-egl) - the NSP script's provisional profile said Vulkan, and the capture boot crashed in the GL layer. → [Telling a GL game from an NVN or Vulkan one](../reference/rendering.md#telling-a-gl-game-from-an-nvn-or-vulkan-one) +- the NSP script took the game for an OpenGL one (`main imports EGL and names glShaderSource`), the GL shader capture found no shader, and the capture boot stopped with `[nvn-mtl] FATAL cannot read the shader map` from `h_nvnQueueSubmitCommands` called in an NRO (`nro8+0xc10d0`). → [NVN in NROs under an SDL main that names GL](../reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl) - a jump to address 0 right after `glGetFloatv(0xb12)` (`lr=main+0x2d58c4`). → [Which GL functions a game calls](../reference/rendering.md#which-gl-functions-a-game-calls) - in a capture run every program links with no uniforms; the game's first `Shader:send` raises a Lua error and, on the Switch, its error handler quits (Any Lua error quits a LÖVE game). → [GL shaders put together at run time and captured](../reference/rendering.md#gl-shaders-put-together-at-run-time-and-captured) @@ -342,6 +345,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - `shader_split.py` finds 0 shaders in a RomFS of zstd files named `.zst`. → [Zstd files recognised by their magic](../reference/shaders.md#zstd-files-recognised-by-their-magic) - a provisional split finds only a fraction of the shaders (3,126 of Pokémon Sword's 41,516). → [BNSH entries of GFPAK archives](../reference/shaders.md#bnsh-entries-of-gfpak-archives) - the split found no shader in the game (0 shaders). → [GLSLC outputs holding several stages](../reference/shaders.md#glslc-outputs-holding-several-stages) +- an NVN game's split finds none of its shipped shaders, and the ones it compiles at run time were not captured: the game's renderer is in NROs that call `glslcCompile`, and its whole RomFS is one zip. → [GLSLC outputs in a zip archive, and those NROs compile at run time](../reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time) - 24 shaders before the control-length fix, 1,355 after. → [SPKG packages in KTGL LINKDATA](../reference/shaders.md#spkg-packages-in-ktgl-linkdata) - `[RED] ... PKZ version 0x10000: only 0x20000 is known` stops the shader split. → [PKZ version 1 archives](../reference/shaders.md#pkz-version-1-archives) - the split finds no shaders, although `main` contains NVN control sections; it reports that no data record or code-address pair identifies their code areas. → [Needle shader containers in main and GFD archives](../reference/shaders.md#needle-shader-containers-in-main-and-gfd-archives) diff --git a/docs/images/portal.jpg b/docs/images/portal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3e45758ef252271750c2e9234e15c373be7736e9 GIT binary patch literal 88746 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[o81%k^O;s|-YmToiSPS{@lId=}Ahw8O7G21Z!pWmk!e0D% zr|7K03ZT>(OQ~&oO>np9z&=E5@aq9n*cc<{VSj&T>r4+yxFM|fqYFK%ej}GEo^Gf6 zMYWN(^3J&X!P|x&)??11;P6Z<cCmfCql`qRc<t9>YmeGXRNY#Vw)E4wKgO${0t>X# z^NUs54r0cy@9an4d`fR%jg{K2cU7M>b@g?>*8Gd`-DSWbj4A2da+_4%JGw%(4a;Yx zYbL6qPY4+;3=IwTBB>+shhanZyQ^uB#TJEvp3gs#q|Cf1XBv4k+gHUX`bz?asGU(E z=KJZ<jb<PV5N0?3I0}jB0rh66gGxwlCjXSvQNWNfh^9%sy1u{=DiTbA@5EQZ!a+O? z0mH;n5jnM@KwXt|hANDshaSW*=9S>07U02;Q(8&ck3EmTVY`bP#aG4209R;~YKodw zpd00uo)w2Nqkz15wA$WC1FzoKg?3!3sG)4{FM)_J^Ma@CXL(hg&yoKLrbV>IT%U!b z<Fu_RC0caOhVJD=+cGQ%>$q~)yEHX7-*Mt}_rIm?ik*;*IY>QyiyU0`Mi{L;mw?;f zSgt279eHYN)znBm5Ob|^xGeqhc<P{kb8(?l4ic)($&QTFrWcSrz1>zP9wfn^6L~^# zJ)Rl+*D|ORpjMo_&pS6+ea1=up@YFyb@@oAw_jQl7Ca_mbkq0vCC!Iq*cTkeIs<(< zEck18cVURB2{u--ZuCS@u9%VLUbFGFzIq7J%$j3*K}5?=^Ly4Uge=aqkrErOww_Hk z)00Kl(|agoC%3`iF)RMmUX;I??v&<!_`Ntp%b7b>)7`QLgC6jQd3z<lH{-SzCy*V< z^keu*NaO=NB(v#+g=GBv0@ScX+8Rj2&Y&0ngX2TSU=fv}MI%)GeIOZA(~0iPT3qI` z<w+6RF%Yn|nAV={8SiZx#y}A`0pnN%w4a?97Dxt)-&~Ux{``cBWU$<l|6r-8<Scp! z>mrwW@bjFGaXWUOl}f@=#5$0fo9v$7@VlsgqnCUU9OZv~Hn~GCqi}66`b9YMYV03C zk=M)P&nG1uJIswEnab_{0dgLf8w)A?mX?)rC_GfUjhux4Z=izFj$Hz=ihh#Ub=c1i z2@<KzQ5wJ98f6Y#;b|W?$DY)y*)lg1F+yWf`kLr9{rQyd>@UKg=MxO+Y+KmF3Pl8; z=2+GM2Yb<ZSR0k)k^Yarv2K)7KA?(#yRugU>kB_X-MU6sbIU(kAmKHbtr}k6f<1cp zd9wu`as^NdBVF1Zvq|=lcC@0FtZpAudGtO~QNT==ZU7TnZV|V!Ko5@pgkIrPntcqM zhm8J~Jt<Sej~<D?<GyU|<T$p>(c*Q1-B1R-90+#Za$t_bijQee{Y|XwR&|W8iYjwU z;B>5uPzyQq2F^A&qM3dqNb@%c=D&#~zV)4oL6PyBaw-q5)cYPP8DTS5O4}_2uW4U3 zLDt|xN1}RMbiI?tDp>2H>UHqdftlJZ58YVJ%GibNRmxSm=?i$r)zlcAjQEBy3BHv8 z*D+H&mDVQRV+-how@YsT|N71=HCJFlY0sJWKClf1`$l6=t0*yMkFjml_m9%^9%3aR zcl-{x$qQ@tYK6Sl%P)jETBLXq8eq0zT??uJAA=vcQ1&n5>gP(1>Ph#Vnf=71WJNl5 z=o$1KbKSmpiQ5*sg9n>CR;rz<$BFKwGwx^^y^A!h2^c=e`@1?!2Qc=E+H1MvGR1ML zpk>wUuza5ANUJ@mVnJw}Eb!1*H*)ED+s<sEP;@|A(KcH0yuu9=7y~kwxYpw|aT7l) zXv8@*_;5WblUa=kF?>hUIDbXy%r_?DO_dTKG<!qoO(-x{XpR<{JVG3w3l=4WrrUF# z)Ku+Kp5<8(_g-6#Q@`$3_ny_*GWg}rszvcwnF1U*q4G@JAp>^1t>D;43_j_f@B$z4 z!i%sTgulcdiy4K)M8ICFh9EUn<wmI^>_GXk_qh|kus2IT2zOdH%(5tNCN6xdBU{v< zI9O1a_C}_^(Nu(MdQ(M8!QJ$U*xarePFi06L}c#Lm;ev^4}k5Hn@>QLblz^B=c&sk zYhcA4B7@FT_9S%%qnr?9@ZB;~|7U^rspCnn+_zUBE-0+43EfTZgRdu2p%&){Rc)9o zM&i60B<ZGJtb^N6G{A~_wx}clA(b_!mx|z)TW_}@<mU=DPwA#>Q+Wms18v~|r~U+U zQE4xtm`(0Ty{nS);cpMVX?o)!gD+un^Q@=i2ZpazOF?bs&}rmTtr|8z-;08WuqF=z zT{A&$QF&ODtdXd>$vn}5B=l<h%XmJ8FIio)ZBsU_zMD)pj!*Pc_O<;u6mZHW@syM4 z7#Qhn!vc_)vd#4teVbtDV(<mK?MgY0lLT08ri7+`%v!g$Gp<l<le+IqUhuW-a}*zE z61@6B##(K&xVljuEaBG+q%`Q|_F&xG;hF)E%x78xFBCY<`B^;+LE)g3I3+6YqxS>Z z#-okwgvWL~YU&$yA3P9Z<I4@~&Z`=vf!A?u?Ke6dX7?qTv){zMH~y};C&T6t&77g9 zQJ-=6klWC&^>L`_EV(HkrbGYwSCKnbJ>Et5?=tgC`s<raU{skMU)T}F0Xb8$;J(QQ z<U2oXctZ}Z@uz-h79p|Y5;^6o9wqiONVpGF|E||yUt1uWNPXf&D!+F1LEqQCvhpuY zS*$icia^I}oWuM(k>lbCp0B@4l;rI8H1QtcS>E4U`HJ4a9sRsTxMq)?w?|HeE+Q7) z`)sa4{{R6y8>~l&=EreR$vQ7yVJ6r6R?&)|;-U`BZ7T<~e;;W?@JQCRGzo=U#J-t6 z=43M3$ME=$|Nfe%k|DEb=8YIhQ2C&AJk9iyJ?lbCS9pnbaVxT8*O3rcj$ytx1i-aL z8eJy-g}!rQ!X;ry#M_v0=JtMhza*+M#1voWQrg@%A8NzxNc&9yEcnxT(^r3_gwND? z6Q)5)t21kCY;f-25+P_@X<L^@S^PU=o4c_3a_HWQO+&qmZ#bScbAkir8@b{l7Q~o6 z`{E5O-P?WXT4()trKoEk?eC`Qq_)Uk#Dbp{HJsy{*lv;rQ>3$MFJrQ{2{`LNYcJI6 z-y+DJcrl@8y2s`Gsr6TZu5i-JUfUW7zs84O%fj}B)r<xGgVN3m<d8(e=22_q-N;`$ zS$zem8|B2^JDrf4eWjmDlj3z9j4!ZksqZ}K>HEnu21U0nNsq}L+HpO3%`*gtuOx=u znRdnlYNIkg8H_vTE1#+B6r?hFUq>C@mMrex%G#CAm4@s?G~0jj!^V)KpME~wP#OgI zI5i>hr*^c%{>1GJa*x~^uS{D*nkxTvgE(@+82<s7F{Y--$lzGtDAj=y!LbGjRT)6= zWR|DJvuNp>9)|EIb<W^;p3=s6l0kKa=gHzFML%P7DhSXx!9sDlM7Hst`2@}4|6&`? zb|iEqo~QY|Ok}Uey%jPc8_Gg-Sae(IAoM0EHNbEAtc>`uWU9`0uzg<F5_EQF-)-*d zcZzk7U|XmxSE4`ACnPqZ%#$1aEYl_(e3&alego@XO-kY8Nn$x8fazyDn^+dDRzGSt z!AzDbF4KzbY;AL8_ve1s)AR09Y3~N&XoI%*a}Di-;@Sx;Ux-NC&ZOE)ugs8E=vHU@ zxtBmu3%ozR81P`B=<45e74&S`+rD^4u4q>=6NCn1)sA7cat_zj#-#OZ7FMr&!1IW4 zhN~kOfsFcKVw0lRBs0>YsEI3jEA!kAvIY|1jj@tGZo8;*n|YYlLGT$0Su~}=3mz?g z?mZ-16G>@SZa-`ijVh#lYr*;&EPdbz(|O`5Ff~^u&jv#gFe#Z%yl{VcEGV=UQPV$N zHVA5$uEn?XYT@cAG)vNZZ9S-NH&6A9lK?NhRvG!JtPO>ZXzH;4zLdOL@{7J5rTT7u zxG3lr2wX~W48)7+k#rL)kqT#L#KxPN^NVAq=!L(m>EkYrWa7(*4&tAzm|AhNzz?}G z$(6ZemdsOSI$paS1ish9ZRHp-5W2p&T;+n6wPaA~QV+aD)a{5r8>9Rqp-bpMr)yeF z%AQaRBz%f1vK%I>F@RH=PO!4XCA6WM6$!n8BI1AuWuk(0*%$q#S`icuFHVaM(*+Fz zRn~%Lf2l837h$AehEp^Cpm>95X0HVlz|R*`CeQag>*(Xobi?xLpcJ>6vpO;R`g*7Y zL?Ta3&je0viKGZ2oyFDG(e4wfNS}VI({>ZREvFEOj5?VN3CCkC2W{I&m~g~TOdkv? z`b~^?kR^zrKs14lfj%KI(Zm2uo)P{!0U5JV;RZjy_}L&1e%dK0EsY>y#b)FY6E!zb z`2_Wz#7fz_E{oCcWQ-Q~aAKK)Td#&_kYp|RG21ExemeD~CX27Fs!T7pQhrvyl29h9 zn5;?;2eU|33L#0;C0|(K$8@t!nCJ@&Fd#szF*~=#qovZO&_Oph*!859;Y*!B4Kc~6 z(OjN3v}Ba3x%$}sD_kHQ;s;Lw^J|lfgUbp3ytE`_u(Gr~!qrDQBaQrlB`$J{FM?B$ z`!_q8LOJy+_%__A;0~s7M_0RG1$l79jD1t4^PkEp(#UP*N1al)-q<BOR?z`m0^ek7 z@ZFZA5?b~9zaN$j!{<`cJNI3|n4RnK6v-YJ_wuzbFFv4%4oD+bo&|5Y!PAUJc`X}| zN@7<A?=BfTXvdyv`vKEx$FHYW2)>9kK4AxQG2AJ5F+VN8-LsKI9&!g=+DH$T%tm|b z%3k3a!ymJsc(^;B(p0&pcJ#8<lN7Gz0P$sh$or7x<h#P3UKg}4s~4HQ+#)NmJ22C^ z70>XI@mp<;ku>k9rF58Jo=j)Zdspag+ih0k9P8MmSj}Y%d+2>yk=j%c-)_)mtut;J z37KcEHBGXNeO4GMxQy8`W0G}{_%-S(n(ljEF?P$_Zymm}E2Tc`kHQ2zUk1Jv`K$d4 z^ISaW89e>$G_gadPGjspAO<`>HFzD#`!E`wUVHkaBjH1VU7*KDXw=`S-A5UMU>f&% z*0}wE-vNJ~yk-6iddT!LsO`37_tl&nruWh$29sDnv<ALDJM`MsDD+QUmC6rzzlPf# z&f$B_e%0;zp276CHsCx@9mslOoS1_I2sHpV3%b1XVD2^ac6JDvR5q7gY38o4k4C1f z);fy947!KW>(9%110^FjnuAyN^_?`CvND;kD4L8mtV8z+yL>wRo^*tJ&D$6HI1Mv* ze4w5BjO7x8F15p7^`E^6aMAqu32!RzS)~l4(14(xv%sLY$lm<n!vfY|y_a-XiASd; z!CZ<UlvttJz7@h3>z)kddMwc<J)sRl#-Sv~aeXgw`}QQ>ETp|kZ%8F78#TuAAjowo z0MV6R_>UmeCq0#<ugbtnhbIlcg&@h#nNRyCJi$ji(0!bt2LGb+rpvxM(k=?;af~UE zJW5Tmu`@VH`I<ryPVAq0p4QOiXFDeHNUR*w!O4*Fsc@TKt$-ymxUIC};g;v*b3WeV zY(<uQHzNAgyzfFuIb!+L@48Fpc&V%)E-N-GP+YF*4X>+TN!?-9iZqUy{yFjd?Hf-f zHrQQ;qXaz#=u~I6X~~BopmtN5)p2?@@@Rv3$gbkG%~dze+f|w;za3{AN={5;KeOI6 z-|djUoUSKIKci1c>s{w-s-=UT6|?EQquK)vuvAsiGdkpthMQF@gTvSGGt{20eTWwS z%$;3@EcKMHIIzOshNi^V=hjFZ!M?txuJK7?mpmCTTxgMu6)=Fxg0x3DnMF_LVZ*wk zCK>GZl)u4dhh==$mRh#&!`zl5JvNe@0q=JOhW<NT?S~>8ACjQ!=+Il*O*}r%(}SEQ zc8s3iS2=f;X5#x*QC_{uLk*ZRFVXA0jzU6PkJE*z6A`qm{O6lGfCQ6n)X8W;-Gnnw zCBwe@U;VyE5=I9Hphb!>p~M%B$4V0|c$Z<|5D&TobHEBh9mF-=D=J<=&p3X%t>BMZ ziTWNuKeadh?8|3I^6p)MxarsOr#ohGqQ0|Y_YL&VS}M1_lz^OPIuUu0`hw7JQk3Tz zlX>t+FRw!--i4g4Uok<1`%rsq`Ap6Hb==GpOhK`Q6pK0t9yN4yUf&MjARNt|ph zr+hfy$)2)k(hrT}v0n>92+E=vAo4`tYMNfn3BQxJVZFCxtCnFJsO|LHZ563k`suWF zHumF`dc>RW=byj!-3+R5wxbIyxV|skO&la7XsVSlP?Jb@yFQ?~g4W~Ew{h}SW|IC0 ziZI(}R9=s$mIDmIc~&Nimo_VO<ryyYM0BJ|>p=>M&?0G6n2|l(@qQ2QqdLWk4WF2w z98O3#CADk)v%y62<C@U}l|S%4c58ieF#^ga3E1rKo$1}9Z`{fkRQLAcVl~w8^;Sk| z4Nq!cM3&H6+YR$`xysUaZ`4M<FaIF^Ej8gf$#ts-X36lA{&_~XX5S-@(soyTj^%5A zuK7Aa`GV;lNsht_JPW0T<$W<MV=Zn%BF22hb=8OB0!U)P^7=W4bzez8O;xg-TH;9j zP1+iD9|r%yJO|!`S&QSfAgj6sfgxr%NYa04$6$7Sy?J8G&!Rlo`K3<UJd@C#X=XZ2 z;}*08An|o?9CPFd=c<<!SXrNSiL6Vdkclp}%eb(7a2=-$?A&v5uBD$G{lk`Svsox~ zncn7?&b{eRBv&^goa*<sVLib$InHXZFj7TA_Z}fph3{vHRBqy?Z^SR^qDDm?WR!!z z-)-JIC^seaoEC@7zaL3ceN@C`Xtp+cn>@N$`}5BL?fen0&{5Iw39#Y5SAb&EFApDU zTf-+XA*ybE686Fph*$N(-LW3!j;<>Rwf<%swEvDl($4NN)~R)%s-gkO0hIFJ{$z>a z4HVzV!ZT8Ev^p30kYpZ=g&k<|elPv7!}*(pIp0!eXFRI6hQ;IKaMg-V95x^xqNT~3 z&+<?*C-@Y|oZP7yihktectW|;Y15}TqC0Ei3cXB``+5I9VAsu_F7F=WF<!mLweP48 z+NRSH3+hx+2pJ4;84OinoQZ~Bk!D+&_eLl)4#FV7=si|UItSr5ffa9Dy_G(I{s;AE zKhi&nj<F>8vHrz(4a-^VbURTy`d?!L)Z%`C2+)j_l30&K$^PSb>pzIHQ^x;C)LX|j z^~P`GLy##VNGV7R=@2BOV<3_e(hVY=GD;c&0YTUZ5hMmkceiv5kWjiC#u(i>f9Lc4 zeV^y~^PIo-Iy>jSuls!!*W@i+i8Ugi>x3E*z~d9YA)!9TL^&x_S<h?WA7X+H69mGC zU#*R$RC;n1QL#r8TjA35<dkSW^^I6yFmGpIueM?~9gor&9$7?)O6vdIVbgoK#gEGM zu}Ko|C{Z>1;rhwH^_QjCuIY*3lBrqoE8*U#+{lhTm%tVYZH+e+tc9&#R1SmZ6HYUK zvz@Dgo?KLqpJHCstuLgVGo(APC+b{P?ubu*Ni|VEAMVi|xVt4g>@a#6Ec@Kx3K^tQ z+W7HgHo(CGND!vOS9|nVf`3b&@lbalY(216{CC*hAmXbDW$Wh+H(TU#tORxLQ<GVY zVGGe2XmuN&m>JQ`3Jgl4H_qzWS41)c)F@<4!X0>4eI2{Ivqkg5fAC3jU$W^wd$mz4 z{jRyK)Lg&{ACWX!pk73>JaaYbM3&Ab_YP93;~uKV&is*mvZxUsfmZ&mM6xoZ{0RRO zA6Ef$y+a~Rgjr~LE+g;0=!i<1bYMiN&c~Q}eC=Moc0{>!%^d4b3_%teL<*SVvSoAo zMurb-VRP{OQl(L0>g7Nw&ia04jMH-XTxbo_=cJHM-BvC6xf|8pisiL&hT6c1oZl7m zAEP<WPpRI#j?raxz3<%Zeu6b#va=#8y3azq79AjXvrSX<%wUnx{h00qs$o=7FAC!9 z-$$mM0ihRzZK5Bv_X0Ru^xdrk@Yarcp3sLk4~%A+#Esxz=?n<Sxk#Q6puq<Py}NVj zN*(*yn{BIifZ-Z5jF-jdSm`2K7nAeulmI#Ljzz{&^7O`0S|W^qTRfBAhaO7g=kPcx zpPUm-y@2Q~2wDBPN7O(Nn(&8Ci!fPa@73~h+Hw1pp7GL5^?m<#KR?UGG*1b68u9`f z{4lW_OfHPJdzZDc;VKB}(2A4GYDa-AGgSJW&4yGi<I!cV9O5nU!XF`hq}Urw7A<S5 z35oZ%)HXBQ*oY>)ZL-SPpo)cq*JpPer*mBNN^zh}u_-?A7ZE`akW2PSTKJ+EaY@*Q z1GN|;P;C@cu2(ef+s<r|zNjmL%>?jdGbO}5Q5mR4$58+>D(DI5AE@8(ALycJ^G0Ls z1I_kRomrRi+|t~ZcD$v&L{b8>-|^y6SE1lZfXrT#CYlhOeaVd7<l&T`bKTURzI;Td zZ@wJUb`Wov_VL3l!4$+)Bjmm`do=@~GssNw;C9;@Vx`TV*yFFqy5HK@xjB88q=+AV zv8iYjf`Knx`{HVCU)hL!ZMTf9ARX8DfWhie%z68br{*CB#@l>evqjKnZw{H()e7;` zCO?K-ar@9Rh*D|yj?uhO9?7-P%U#~lrRQSP`&;%YmQ(0dq{}b*4fTkHIX$L|s=?^0 zd}XJk>h)~{)lL5z8^!I9R`khKSTagX;g%3LZsgr~aPtwrs&Hhqm1E4~po6Z}RjmBa zi&>&^GjB+s;1>kOoY2l~F8OW3$O3v?|3k2v&Wb}-kVStW;Uc>>U`j3wD98qz{IzsF zV(eA$xthwmkuYa{*#qnBR@`=cprcy&!Qq7To-y0+Cz`0FgbNDuvh|Bbbr<rTK%s6? z4vuRhhatf|#0=u?(+}OWG&*bNA+7DVHMO<BMoY33`tAX<R098}ZP$>myH;T8Z~CJ_ zHLW$CwA}JM8L>fJ*TfQL9p_N#Uin+=m;PcBo>V<pwWyukUhXfG-g-lBf?3QlO=|lO z&WC)bx)SFK9N5!-J>Cgygx!SsxGV|&zM3k!k+K%gy*d~ELv{g-i())2UhHb3VsMmg zW%<RGXt6>(QJQT<Au_O+hC`ARD%a2UQ*gu0U2VzRwXs@bW@aR}^jqc2AaRZNlN+!M zK_Kf*fmL}+z~Q&s3>`7fr>KgKmmiC2v1a~xuVgS67{H9Ah2(fFBSy>kz?&%5$$(*1 zC$JVN)Tn>&1Er>1odM~sppap|xD`M@XT~3pUfDQ2X1?Z<1&}VA-G~@_N_5&-WWjNi zT7+E7-(}=tC+99)@^9lruO=Ke?mqxKrBdv!e<4-F_1=1R<7>b=GxbUYWU~H5JDqFp zc4XUOCyv=_jUXBV+Y^WM=GYyMF4ipcw%obys;k~uG3P{xssA`P%ZP)hth=1}VP7e0 z@PiN8)TOfv3M+rw*r<6LiDm5c0fe=`M68rA%mle&nh}1aB^l)N50uLte<ob}<}79O z9=mBgGnySPoU%czN{tjWg+?9JJ-)#In1**FTS8_*W_s;=ox*xZl6tNZE+!?YnV4{0 z3oD8B7}<0Xv;(N5A3ry?w7A*N)n1llEI<=YubHlK9^sRc=QmpoK-0BBDkH>q*Q4;~ zk&`K$#wN*C3Y^vBMlMI^*qDsyt@TLNZ8jL$7w?be-;ENFV;;p#(eiai*1*z^M~o1= zWDIb@t$Uc+MvI(m4t_O(STWjWTuo$-iwrwbPjc;HH0eN5uP=SHd(m<ppEnkR)o?86 z=u{FvKN0HUjlpA`v=0W#B-kN|`T_iwcjGtB$3FQ0dh3@6ISNkGFB0fr#2+02B@KFs zlhp>w+C(1_EdjWq`i<V7(GQ~wF0Es{y~}jvL7Gusgz)d8p?Dk;wB=wb80)X-@F-o+ zb5Wu8uZHLMr6)MV+S}<BSG03~p%eT#Q0h<0Tkr;a$+xE}%FKkB3mNwyxu##czhr(` zzMewL+ZJ%`y{QAOuNPpi-k#}KS*dv_^?Fl7BVL*ywa2x)`4~#LRvm?pJS@y!zf4Vw zc%p^h-i{BVm_l-m&X~*fH!{;pYi6Jo1s^ca)x4Q@@pPniBf+eV{57DsB8pI*q-<7h zi=<EUU5SKF{<7TlO>gOB5mDrcrO|gJ;qyDk7(Wuak?|iOfkk`Vx|_z8UgwfZ;D8ur zVFSna)}}3DTGy!`_M5H|9tgv)&A3`SEScr)JE65dW%(8S9BzVrQc&?x-L`urUUbnR z?nd2OidjQtorEIphWkbHN~i%nixW{uj9Vnk|I_X)Xv8I8yK{aU9yT#WB*vbr&9*ro zQ=l#|$i(oY|EBHg<h)jCb>5&FkTsl@xfm~~i+2Aoa}z)AQTR}6_=Hw=nW630!Hy(r z=>a?wF_}r=j-9M%9V=9Z>%hjAu<-HFYb^y@Lv@89(t&W6n88?VeKhjh`Q;T!L}Lm0 zRf)H~rAc8QI$?h_d1^M<<PxY4E@ca5R@AojqpiS0eZRlo>nJ|VVHv`n^qm7wtevgu zgU~~{{@9l$0<r48#J3LjP9JRjb$2n~z>kJt4V9lN%4Nag-zL<c={$zi?!8?SG4IO2 zwYWoO>*HOHc9WNnq~AiN=EwB)#zWn$)Jegv&<xRnN?&@vsynY%jO&7ke4IG8Br@cX z+)?5iXFL7&>?@O{vL*9M(6vz`I|h_a6@FvTFMgxR^(xS>Scxghro_&b*57Ogq?##h z42`ZJ9$PlQPmK%{c0<{v280vVEMljk%vjwrl^qZy>G;->6i3b(8|J7;jg1?2sF}TP zCHC5n8|IV6=}bqK?aqg7k%-ZgVsvzc!z2kG>atIkeYM<ivuyU<EPSZ2%x~fRi~vmy z<O(ybmb|fHP;*!T6O;+=_V`jo>_I&xzm)y%T-#hem%=4()Xo0*s`>qR;Jtw*wWLQ` zDvn%gWb-z9-F2jSET8&3Cs|qW&v&i2wpk~;O2XfVeIV(sYu!mz7L3(WR~fj5Lc-dj zXfh{|q}2(|rfA(Q9XV>KjB?lOm@lpeY6D<Q*GBK}$2^gO_7^i<8*m8><WCa^an<zM zVSfvgx`NTV5G4N;)sQl*h^~0vk|A{JC@)9koLbgRNp$NT_B5-K#Uo5c%0UX-T+XO3 z#S_!nzaceOi61K!P-r{)t47Plt()A<o++k-tL!7$<w?;Kea*+_>E4);TKE3<86rQJ zL*Y^eV-Lgf_Ic8i*WHxq1H@L-yPc$Vn+kcD0zH$YZu2FqiJ=UfhxTW|u=<bCZa3iQ z5WHHWbnf#e;m;$m**6n%!Hu^1VSI`UUB$L9S8*tZal4=Q69KtFJRx{RQ=uYp9S}&V zD8qZG+9IeoGw6@X0`=bcdEydbm5{4kb9eao0iN#rtP1hd{-Q=<E}lzvZVHj*pw65p z=LE48BI*r79`~m2P<||yP>Jb^C*;@C5|H1(y39QA8c_$|o@x#S6xJ4#GCMfqOOm9W zNlv2USh7jmsh!{gclF09X>FB)898aBTDE8gjRIPpy|`2GaM&Flm|Se-_zcgq?ffrb zzaOuTRLE$_7Nf?dJ@UrRA$@YfY{B-kTgnVKI!jv7Nv4)@iR{sUI}<VR{eh|aikvbN zBbwlj#<pWtY*n$u?gO}}7$i~gLDEdZC)gPMt=Q(*Z5B)31Xsl&?==t_WEEI5{?~u- zNyE4AE6Pkwk!$U&q&#!AZ*`f;Z0Sw`=g$aQeN8awb)D?-8&1uokLG>a`pVT#!WhD} zKb2*{!Lv%l*|ya5>h?@wCY+4xr0VX#mUQdzk!sLp)JY#t_sjnGmxX!uLmknf+wKUA zG47RkS{m`y2~(DVl7IQoh$Q-OSH7Q1Hh;R;^fTuXoP_ir$gI4B&L6?*DsG_wm7ZX6 zttvym<!kr<h292J`B0!ars+HA0Q;5ypsIH?KlNQ@>UPXcn8pULz_lVnqCnONKAq>4 z7WH{r8aXKqMmK<X0f=9j_)is3F0)3oA`O^h`VxzR{&P+I{c5`4nnCstw5~D%#ij&p z_2lv-dn)#^mwGbX2b2QHk?p0c`(-AZO$dUZz7cEOaS*e~SND&Vm?<gf?iQe~`+G1R zX!EmhR|NODHr*Z2dd~gDdFHQ#_vX{|22Hs_7peZ#^TnO0$1z52k<C{Bdc*h~9q7;# z`G8LdQPN;zgFUv=Ms~}sK4ijUX-D?9<{mKLJA0AGy@?==xv)m(2FNbcip0ymClM`2 zXHWw%o%4$+8KZ%Zwd~7uJ}y1xX`MPa(r0GX1cG}4?Jy!t{45qeh9vy6ZF>Gb7Mm9& zd(UkhAmdMO62lF%$6ZPfW8y2Mnw}oF!jl`u`>TmXY=eI7AO6g!Mp-z1RHIkVE)*~p zbVb3X6So=ewUHZP(9}GTQ=t{0$0Yu5^*Er0nv%TBaV?!(irTxvv9-DVuK4@8ERZ>M zEnnP$b1Ojk0C&8z$W;Fhb{yh3FU`N5ceOZeE2?QO?5(xjRg%rW&e!8xk9eUr=z5$1 zAk;Chyj6eQk3;&&M`Cci^D=f5o_evfvPIQL$|q${%p@O)dzQqI*bsKmF#z^0Oc2R* z#v{|KfH0`k>1OP~f0-i1qk;5PPv>X0ofI*w9v0^Nny9blO>sLBo05m2QC=kT*V>fF zD4N3HJ{t65OBcFJ6y4Zk+gBnTdR%<5hSg0Y9H{`JS@qs}S(X8j1R(UhEmeTRM8|GQ z!7b3Vk}A$<(<)2kv*qw@i%1w~%1g71mzYp~^w}M$RD#^x>I3{S-vQ=qZ~}0d#9D>U z1MfsLVo`RVtY{cCx&#=7iT@&<<P{VgY^caqb9cB?jRM;|EuXgQT1AS24iwh_d39OJ zT(@sP?_&i%{+%_%Ak56yfjK2U4vS@R%e$BsKjU(Ps>H;KXqstIb)W6F-hV2%oln*5 zyy`byQC&3H3Itvc!mi%x!lRB~S2q)Un?+wth2p@6H``LY?U=EWyY-0l;9gPDpL5;f z9v9^NVQq7Cf$wYL<>U9*BSQ-nHq;e0m04j@QAlpc*5i54^q{yRnH>#>zlRnJ7B>8d zr58sfy--%p8(EE&N6xUuATo>^e^g0X-zAlEg}&>SUCV&AemkA6zH2DYY+hobytV2M zu&jD<6v6~m=1+D_M?Np>hl>dwb0!2&(ctHg33XJI0rlYyi&|CiARjnvSpIP{lOH{$ zUho@Ed@(fgK0T%)>t4qvF-WQ1XWj1#k}gii)5y+X@-T2Y9o?9X{&*jpeVt=V+?I~W zB23liojz0T>KFHTL9EV3WZTzrQR*E#-c424xbtv8`*(^pOTBG^v>$mJmcASO=;%lF z=PkR|TV_;q(<40439}S-*a|hMsIK65Z0csH$Ph<0{gx%UFM9g&i)Q;st-VTWe1a)i znD5`v&(>$@ZG?c!`x{xsyRHpYT-#(B3z1+$8qjp)`h}N2IQmw=;k1eqRI%=fs<pKA zi&uO+Exdk=-B%?GJ#ShocW8(bj{P-$moiaLRBF3KtuKmCVVPXq_ib?6#;`JbJ)K_2 zt5jof<{I{m<XidsHvL3my)w^TIW`_7my=yPSHo|J-J|~#E%U)&erJ(~@(OgHp3&iS zNAO}^O??0nLcjBy>cIA_J?Y?yHDobzYAiM`gnX~gmq}G7xd>qO`K4N6pvxA#8?2#E z#8Fv;6KwFOt_lp>cV0-OLYXHJdiTq|mSmvOO<Ol?Y%<sS!BpEj-wBgYE=+Kwrg!H7 z|9Lfb-?){gxh6tm+@h(TLfQQ(ia6{GUxVlS>HS3FhVv1J_GDYT-QCmTCa3pW&RvxA z>O==_gh9#0d1*QE3kiC#&vYAA;{{tE1>j3BR`j0@^pzrI)*%~F<>{C0+k&UXO49yo zY&ky_H_joeba-zEN}0$IsgOj~KKcqjUv0yIO2%97oZje%uNK0pIf3l7`#5tWM+L37 zAdoW+!v*$oqV#n}_4mtq7lI5_F1P=1n5*cs%5Boql9`{Jd4@j}2qmuKqRRX99_}wO z)aF(m4I$Bo1#8iXAM)-Ma59VAWw;!;1G3Ay%4*SfzUa$|klatqVa%60aTJZP>0=ZS ze8d*~-Gl&ih++qHze-Wv+#Hf=fu61k#`aQ*7aoPoG7!7vw5Qd=j}(?fl3T<^f{frw zUr9%#Ycs#RP23IE$)A@ca#eug0yyKNBM*v51SsK4K&#Sc@|)mV-$Q*?#aVZ_SW#8} z_aDO~;70aR*`Lz`wb4>1>t)8GZ-aOPify)qK68E73*2fG89O{HDW;KaNxzC~2dIit z$2XC*VjXLeIs!4q8oUqOioib@+eakS1+kN*;d+*|#9T!jEViO2C@K|-O@f6Rpaqq= z#ZECyMqQvgkaG2l^<{@$wipYW7-SJL1~GbfuSd>yY)<-@XL0OqoRTLvG~<Ixq=h1F zV;`9AV=3_uEdzWdQh(r;JS}RkPp2k$r=fZxIhHd3O&BprJL@r=`+SL5U|)iootO|g zg%j!~Y`QLaGl)r%QvPm~oc!?8ii44?<3>8s##Sq$X%EbtfKbV0CVb~cA`ZVflo`~E zIggisQ+wuyJ!-}ar$6wKbdR3=KE5ML#}FW)Ye=P<Sq6I0(aZ6Usi<PMapdT1wOHfP zMpIlgsqB7_38IYDy`vzHAdhbO3eNGkS7$C!c+LA%$b><({f1xPgcvOfL5~)kLqvS7 z1x%vxqC6}U*Wi9ShJf#5H6Jbm`(($G_Q_QOrvC>Onufp{!qwN1@}|+`-Zy?xpr#=2 z<h6UU{nCkMmuKxebLMVwDD?pG@nz?=XvpuQBBNx3UFmm<=(*WsCPz|UCCgVztb7Wh z??`<MY4HS^*u=!7FN+_;?(;ljZ6b!lCbRo}NX5E50j680Wx)6`b^SN*b@*aUj{;Gy z?65b)+?QtE6h)&Yx{P~0j%iXFFQ7Z=?~$6}j8*v@ZYbI-wwrNS@KZz3b=^(F>DFP4 zP~DXfu;s}!hI1bayUJufU=%f@mG1nhz_k~N)L;gY$g5geuOk=J?TDl9>~gth_`Z{& z3aNC8OAAIiX#FBwjzf)MWgni)oDWH@;NLwyCMI>@<$efug!Qi8-@Vy>-MhO6MCBmI zSMKY89xm;>*~?W`hz<ux1OTG0hg8Jsd&yKUE!HLV>{8vWAh=rddRrVrVD0OSf0O@< z#PCNOGyuEB5v#-hZ+DsU^3}ipe@OCtfF#$NiO}&6T>{jY|Boeip9GLt^hL^`-~ZKS zsy7WUnV)2jtQjGxfGxO&VN>Jwp`*aM1#}mCH97V9PP!3!5T+c!XH9&zjjvs&rLbHV zq(1^4+MyjMbywyM{_-Se9ny~k*ZdW)X;}M6b^0-+G&}m|3(cs@kq!zu$J|1wUwmL* z>z4ze&LQ!JEv51eb82>@T5-e<>I7PPBbcq8$n~aJ=}d!S?l!wK3DBFWR+~p({fVFI zx%x~rttkl9uU(rL_H23wUXqFhw<QL0cyF?NU1i95u={NOlPH?$mwC1sPl<wX$V`sK zT8aKd#c8v0;=ZwGv*m0{YI}q5?0Y@{X9SH;g)OLEhqLEFOZ|d^lOIU&AydT>h2`5< zd=84o3QP`~QcJI6dkQ&+T_?5zpEU{Q0DgvtKJesaGi#vOyDe!@zJJ#&+AJbQXrIlZ zPYFEQ5aV5-jTmlD+PGInYo@V~qG~Hda>oUGRDV>qwzAXnOZ_c_bm!#_z2hgJ*N+r- zTuC<AxAU*2EDnagUgRlAJ^c15XvxudC1an-ZM7}ph1?cp2Ua_Te?KZ%quk+}dzyZe z|Iz$gpMo6JBIMSE%VZZ#UjzoCAY_6P-#m{l9|){ff6WZ?)&0XKZ8o8&2^Zh2w)x}x zBOV?)m%pZKi*K(vW{=bLg2m!3!xN2`EtKe8VV8QUTM7(NA23i;pe~y(hR71~M=Z)f zF+YmP`L7?aPhI{oJ(1k)c`a|~>cGUw|A=fho4Hn9UO`^=O(h;i6`w8FLUm51hmD$Q z*4vVaA7KbRGg=cXo9bfRG4X8!G?ER--twsX;?7wvt-k+2%!cG?yi&#PJ)Z)?r|@xR z6dCPPP}rXUajn7zOq)j$?PjSc$ciyK>UW69ceUcV!m=oiQ6VBy6_{M(@Pa&x^Dzmj zoFQC%%AG}1CD+7CpF6<nY&ULzAv}4{lFan{^K@s_<#JB`R2EeyALA5k<$Y#XGqnYW z*f}$$CzGNl5sk>p2#HJjq7N^eWMT1Oh6!T63bjF>vn_|kv2YiDxP6Yb8+^1<5CnuA zqp?xNY-oIMX7=cb3k7@<H}3*=2}CCNQuBSZ{lvZJV3q?Yox52m3xT&*ugS@G&5OFA z&R@6jY~LP)Y&Wtvfi_U{!y2!qM_jqbHdHUTP|)Lppflgy4u!*_w&ttszM;G|N$ABC zE`fPut&2GNK+VxO$0<`}j2S@J?GWM{k#g_%V|wG?mq)}BBv@xNxf3x6hyC{#$}5xv zOeVnNEMvdYLEY8T+*DLOeyZuhdj;X8j>D5_i3E<jLlQ=6Vaie$%PChy`zz+*?oab) zDg54L@G&edPbgf-u5|L*HUr|_v45a1`KM1atDnATJT6jbTu&oe;0j{m%oaZ8wRG`b z17po!XMAp@*JU4RLS6R*HZVQ4^I(5Se%0;IvkeUyk^FxkGR5(tXSaa~v*_V-zI&b$ z{At9ZQcdrh?A}@tlhwny|3X*DF8q876j@Rd2l^RYC8yhO+!ALnt>6PG?4{<?d_o$G z333}>XpvX@-aEOa%!lP3{ik(e&*BRmb-+n5*&1ATA-{r(EpcBECNp@{KaPfW7-cU5 zV)1a~0SwU^`VVAPYDs7zUR-(1oj%}&W_+ierx`1Q3UtI6N|SiI{saB)K`-~CaAImu z@GntpD9mRpO+4t3@-~%u?)Bv>i|;(a7$Y+}CTFb$@x8dD6Q&ll!39r_(Tv7N`vQ~j z5Rq8sB0lFLb-^%f)a8E1C`G|^vzrkbeE6Q*M7-8usfn*w`OO3Eq)YR4&dUd7z7#jx z?$6$`^yyMV)^+q5d9&RzU8|MA%e|#t!%rA4t|o=3a{i7fn2fLSVnBF(Ni67El5DSi zvo@pD*ubq5{9HvQI$=fX8>wPpySN|4otXTZ8&7Sx22AxiOM)FWb+0TYrM3)m5idrp zK(7<r>$6&rXIM{y2ST3~#^rdi>pY_|I3gu1MlAFirb0~4Pm4cQ(twa+MTb@)wCSj< zLJph9^8J{Q0M0qdpABew_sQIM_gKBDrKGqhi46+3x&wm5WgLv-wG(=S#4DoUGnOIZ z3^S`gh~;Ja!3`&cqXM+<Zu~~f{6+#fjTR%2bZhl(TH*`oWHi>8SK$wTv{w=N-1B@y z3l=Vub4D(((YIEx=a=&PhvbBrt0ha^?apNF_%mgiEk&U;SK3WR+o%LN^1p4Pd(6xi zST_+`8SSyqeUm}JZDlu4KJ%>lw6q6?veXjvdUE<s-BF8dlbu+AM8PpV5)R}MOFo^f z_hbe`g^bvg7K11GLy_XgQ;VfJhdzD@X`zMy5Cenh{GzIKdLEgz`1$H)vxytRg;D(@ zVujO%#FX#S^ghHNx#fee7q;iP9}yh|nMr>ux#uXR)6RJ0exXa=?$+X~8y*uCb3ve^ zK_YXv1rrwjNY_mvNE?WZC<tMb#m_vs<?3g7XL(pK5R*fEbu1G#Xgr9YiA7NntiQBU z_?b(#PJujq5MQLg0nrvahk~PqW5xT3cvtou6<#T51fHs<ova2|=!xD9*T^+gU6U#* zpaTNWxI74ATbE4!a6cBYL~Q71Khu}~r3>ZQBP<2E0tsHhPbUY;X?ejLW5-3}*RPyj zeV<=Lg6XdRLy-#!)vB*P!J<Q_Cm<A0gqtbxdzCf&irSiZ^8&Jix?AU;fuj@Pu*DL^ zjiT?8?uOsv?~aXf$`OCkS))1KO&JSeyc5n?*neJfl_EvvX-d1XzZLLMN=f%#X7ooo z-S>?wWeO7G10v4E0T(z=C3whyLc95MiKs|cyeb9$Xy;*j+gS>ieO!zD)2jI2WAv9J zXqo=}Fk`3GAI5ZKO9%NVGTG~{Hn{r~WwWs>d%VQyYC_zzd9W0zF9GzKfrv}zw?=Xo z1RaUH+AEOciIfHvGw<2m29&u9$fALcB8}nqBGS-Y()k2jnD^zpvdi{}I)^NQv9FI_ zc^7l>hskcuC7I=PS?`;1Bj_D)hIUes8Av!OtmnY!^g;hkU9hu{MrWv8<m1T8$pP=L z(K#ULZ@U-`>eOESaP`3KA86?u$m+qIVJk;=MrtEPF(xm%E&qWi^$IgCH-Y7biN_zv z5WBAt*|x>w2M{w$Hx4a;{Ku?w@;oJM{7^(EUe6l(R-NOlFw0NM#p)^Mh633Bgfb@D zPAE)JfJE`XOR4|4G%OurK;@eTj9mNRYO7axg$&>V;FO@l(-Oz>CMb3tn1f6l^aj;v z<4dZI-ENdlx!?Dc>hGnMu1)fAd+4Yf?{4@+E+LBD&)}_ByvRw6+Db&fc5n|cLF)YY z{k8VgMtO{r_3a8n;%R-CM0kjjiO0Lf^3EHhcGNXf_8VhpA1>yg9>SHJTqs3_D0d;e zeToUN_^@FfnL#mnFObFIj?72v%FC$Rn&8i~CXHwhwVUEY+g0>lE9>@yYO#lXyM5)o zxR}v8SX9Kh{$_v%yz(FDbI}LjEzIhiZ@c0~&s|P6Ts#~(3rromH9r+vo_c-z166_Z z++!frsx#6?tjdsXI$lfjLlg2vEEi7D-7|X{$7QXyY(fkZJ=19mkHk6eG;K<Jvn(iO z=;aPjO5IDl(x)%wrj*pTm{2Zr)HB_(+tNgf=Qur2n28)&b}YECW_}UnoOFG59D4EJ zV7haXEbI%>13DzwEWEz;>1$N?WEDxs;`_j4)M;sfKkSsR8-RCL5CwkPx~L3Bq6iwX z!uB<xJwdd1v`Z#&iv3q+mIN*5XJIOM%yCgPcI7p}@CiK~@ImQa8f|P!Sau(aj`CAx z#%`JZTZfv;_)-)#bf8{Uro7%LOEQSr)%WSwr-gK*C_qHG^EB%@ff4`d0-}9#DJkMJ zV()4A>xcQG%yDUfY>81?k_|m5mCxPW7C%>i@@o#TGBb9m@=bO`%qG9fk#?tRw?``0 zI{Pq;kCE2Ckwf$2h8=4T#?849J^=FSJaVi!n3vUgybRdduX%``4XUm7x#!PjT)DLM zO^Ll^ylGMM#BqS8jk%8e+uN@W9AoF*-q^CwreznrG9n3kOf{R0)x)1Ky*SN1wl`>f zzjy_IMXv(%@sg%xPo{1b{_NZoaB=Ds*72{M@^)bF<}X5X3j118#k7!8kF1y?!dDcI z5W4sIJ9hi1Z?-llbF(Jooc?xZM#tl=X2+3?=Fka?mMUfQJf%B?-^-0>?buV(QZVVY za!qe!E9@(v+YrHTa7VfP^3@yERHFQTbFKTJAT7;n=phwN&7Rx2=BGm{=+R$UNV;!L zY{7wA<OtNxg&+8GeUHzoj;SBX91R~goPyTymr_np8W%rzj{kw6z@w<v55)7U4FYD$ zFTH1)e;okp<6)qd)ZEH@%MNkx-5+fyj#EETBfm#~ov#ElcX%;g4FR4uvlcwshh|}r zU|%}Xi0c{Oss}eOV&{WOpjY>c^hI1}jcW&YsC3BHpxTK{R*7GT=;9@8?aNdBZC9^% zZB|a^!2Iphd9uL9hj2Klpz$=ZHn)9~S9<cMS#VR+WR%Cdel=&)rv5KzI^qXyo?@Q( z6nxug2v_+cBY=Bg$BokJ?iCTn>4p>LB}b8HLSYsP3C(`;Sg=!<j$?tHG22D-xy-PF zV6XR0F{Ywnrx;ID;j`F=vo$X+JU&}b^VdR>mG?KjvHC$KMus$ZQ`Vwwaks9Q0yWmX z%W_m57=+5{#KHKB=GZJHY81P4nk9a#np%Ok?$a*OOLtT$^)OF#PG)8b_GfJib>WKE z<VMi_8`a60;&1WW=HjfKefj(Eg2b;zWDf&l%4Zd&>R6s<KU2DKTzL>fv^O!6AWAn` zAMURlw9+DL8y%l{bBGQhHSrrmZFmz8f3r<uDPk=$V#mxRDbj!Gsw-;lIrW{s9+rQ2 z{2CFW=oD4pmUB4dj-xvCL>A}weEc8?y66LEUFmDP7nIC%zy1f(3^gahj3Ju6Quchw zqYC$5bY0z&7@H<P6gU<`&DivxPdlyz-R1M!6$@Tocy{w(Y7WVoDsGBdwwZNt$90s4 z-#nUb7?av#0+#+Bi&}%ZCce_g6{jy|aH@^|UfUmkogjh2xCyLRAN96R$*swA--#2; z%pX8;Zw6Mo7iEW{Ki)MtUD{{)H8b{kid3!a_tDELk;lD09fj8K2JQ{ZVD@$Km{~Z{ zY4AiQ_8&D!5c;sh#lv8DvZ|FKwqDd#k>HdmvpTY3=XWp)qNz~p!7|Dk7O6!SlH)pU zu~e?&Pmp&per1bH^W4l+&8eXqlwpJ+IT=j~*y+~TkUPT1BNjM$`FlwphOjDYm0U$9 z_?3ls%Je1I*B^=N;2Ff`kc$a9fxz@%`mV-aSp#!vNnhPs8CL;OI$vEGS}FIas?ZnV zTX*%fe2K`EB|RWvnh69}$Mw;O(el0=52>JB>-i8c0V2qknsP5uU7u#N3aEgNL#R6{ z=*_yJq$0*AJ;pAz6;joE`=5)Uso0{<Du;}FrI)-EMBN1coRoTAS`w@l7#1YGZR*;) z8!;5^d8f{dJTBv{k8~oC$o(FqnKEAgUItuQQqaL2??%G4&5uuih0|4z%+CD0civ;- zLzl@y;*QcZ;An5K9$~oAJxTEQagpNs8_JU%3sTQVbmoy05Uk&%>ky~eRO4ON!Sf5x ziW~0U>j&3Cdy}0w=a}(kWJG&7tlt{|8)(=kG^`&e74uvO$0BP?J^~)s6Rf6Sru501 z&rYF}#eH26PONgJftxrQzcq*`-}79S{w`l7vGDP}=16VQbN7qCZt>jJ#N=)cU&)n5 zR`ilBd1kBA&=31IzlJ`?Z>;bg$0RzP(%;p2SPfKgW>Q&^E5JfkmHv?2E9Me+?v=OA zWYJ4+X*y(g-|`)!cwv7?#^~a+E#a=~uR`yE5iaCvcs@go#Z2bbHgkIY6=h~-EZ72@ z{qiV!deYC9&WvIv&)*XU&HbSZY%_Y1CEP6FWEpodO|pzn@#jbkrGDB*X=Y;Qi!e8t zluEhubz%*<Let|1_|jcZrU+7ppqVd;ka^KRx}x!-qbx3!b^;2$7h<~O_M=`=gmgj8 z+|?e8$5eF@i(=GcyjjknVXxMyE_LvcdGS=Sl{TkqOu+_!%rfKR1uwLO4V&I4i{Onw zas{8Such!lW_u;)KTpz#)%N+Ck@RqmdW7nAF%AESN=*SCg!988<-H#&B%l6)t}us@ zc+cu~*hNQ9a$d(6=Sgk9Z&q^2Ny$<B>m@icz{iQE3#aGe7#A_)lx3t))o=&m_$YF7 znc_p9|JU?Dc7G8VSOJ9NknQPzayMQLhiyrKZ~R{nrBhEaeS&TKU%WJRkO!^NLH0v| zSyi0-0HB=FP`sRM3&R<oLU%L(V8Y|k<>CIjaF3jii*~Zgr606fYfMWRaMx-PO>h;Z zP5Dtls%DNChBdeF7c@Pb%g{Bt=m}ce0B%_OKaYJW9NPN0jq1?1*idH_U}%`n%kNEH z>*Y^38O6QVF0+IVv@x^_+MhG8x@5k`w;f8mbv%<$H8BoqCp{|f4~jmhzj<DKL^8j3 z<5S)qh)9kAFbbJV#jVD;q;1yI#@9Mc%7Ka_S0OK5m66jp&t2U8_OX+Ub3B~qM#@CP zwP015GNjCL{1;V5h5au}E9R@Iv&pCB;*B@K(OpgI3#3kC83Jj@(iE&_By|}?`TY^8 zP1yp;6MAgco?9j}5zJ&WKPJ3M7-O>eGaH;jJ<M*X@A#N3G*)(PtPNu-()9@ZrTd-h za}z|NCsjm2dR>**;D@Uz*@L0`hui`u#*YbZO&$FOZr9cQ^$#|-{I;WoC7ynZ^$nzA z5Rl3LMB*20wsQO%wnl&h3vrkrC3Ix@M@x$Q8&5MWsYGua_|n|>_2PhF0Ofj?=m3sN z`$S>LXwkg<bsY+@lN45X^vDX_$+1m?Y-s78h=o2{B7p+k<6m*C!vV@e4wq0#(Y^3Y ztKkf$D+)QeP08_|9>IZ9Z7M#Ld~?E}jnAvp4ZVz{gs8;lyevSp21R`!<w>rUv?XPi z8sC)COUM1SXa(q;wfUxf+e~HQ?d~A=PEO-L5K5j`vycv*VEQ3qS=x;Mzvl7x_OahV zG`TEsNAjNn_zWh`(jCuiUf<hNVkeQ6jKhq=HIFI%)hLA0nCW~%A&KvDldZsXQ9F{d z)WWf(R}P{#+|PTdWazZw$Di>Kzo;VmFeCrrxx?~*xhFMUkLU{xS}jR;=Krpfs!a12 z<b8SI)&ka6<Z!1Rq1I>;6B_rn0ge9bKc`{Sz9PGtG>PTFm_ozjUnUwhmqZD86T}S( zkz4bS<Z*n8k->c86h2IY4H<p`g_g@sCiaTsOu0(r2T$cb5yDamUs}60!|$&LaBG7@ zz~-F=CL)7#a8jnPq(73i84cfS_uHbzLL5g~5Yogzyiz&9!p;<b@_g}JZ43QhCNhQ` zBuQ)i4`dE}Io0}J_x)<2{{y8io6r9blnPo|`1N7sjp~_w6FGDvTi>;=@&uH2)?Y@k z^7+R>>;1krh2sGNq{l)t+fUQPtv^py(-%c)G?oqAZ^(fg4V;=6ZU&JHW!aNsgu%B= zX4D~pGC`$2-@{gv_gpOf;*hdb0`tGiyg!?n+M?a2Pw2yZaX<2_SFHyS8W!dnKe%SB zp#@`JZpuTa&6usW!4&P!|H-h7gMP@VAB=k?u?Scj*E{U3{wbySZ5!dOeax%WcLJCh zmZL<b4czWJzlu@^=lTysz$Qx8hXY@b&OFM^(gK(H<hJ~AW{u2Ns=Poh#dv1hX;Ipi zGKX0+UTx-SQre8rwH=hWbq*Bw;Y%X(h1OE`zBnE9HCUugO(ebjscyMV#Cc^av=PWN z`Vn_X=*s6<&WqCN)Q>Tc`cWNK_~fxqeemgFuET>59z$n72Hb8kdQw;ni&84-qXDFX z!SU5pzsT9Ht*bJ%-tyF+AX3ks36Fy1k1wPuW@q-m8l$a<D882tTKOp}I2sJ=%=zW% z@hM-2PD*MP&*Wl+cB$bkFcn*<<6E-%cGMlcBYk=}5-Qp>`X44H3)*_s+Hpu*g%RnH zCFWsAFInpMnvs3$5&n*jseTo7xMEJltpE$OJINmsr=nOKITETJMVXC*0G?tW$n&Xx ze+M8a3(Mk#ZWVE@4Bwwf=tktiR;{lwS8an!FU)sp+PWb9v^>vd61Ti>9PH+UYvA(% zetNE{qHi0qX*va)vcbsoTSs2Y!fZ%)h&huUV{kiI4DBj*h8ZpI?LVfZ_e!`V#?>ks zn{&UhyZ2ngrT?69HD4LOS{%`WaG4O{<at04ue^na`!?O*M<WWP=~_6IExXU9#5@4y zi?XG@`|Crr*UrvVO3ZN?;3It+`}CE!cEdXEm)@rVKvW^GC1{g(XAPVZ`<VTt=+cI0 z`n*slk#RNslzc~3XGiilYOvwG_85D9skzsKglJb$sw9d18@$z2vQ!bC=32cU4|<0f z43MAFEaBNxKux(wUXXa1O1NUjeA`Dzk`Q5@bPBK`XVd3Y(Cq%vyXn+uM8bOB8*qpu zss8>O%sv)1ztm<A`FQ#=*5hd$F|0!>fU9?6WKgtMP}YR1kk9LYU+)b-mHOvzk;<n0 zicd5XM+{V@BNo_@@D4m&t<q4ocA$X>8moPo8ZO}Kdo?MVT&h92tuP835_ot=w%ts+ zrKq|ZKN?XA9aPKJF9i{60km09d%{Jtj*M*@`tr|_vjj{{;eOnh^aQKH=Y+q2C#*_B zAkhNhuhX?b0C%;oBDb_0js0sntWQ&P_mhMYM$ep>&j%MVSwQX%gjH%ZJf4c{y?;{p zvCa!wYa}fZhfPVRvn@xf%1;mTXAIX_S`+b~N3I=|=vqXF8gVFh5hF75RKI@Yl^x(t zs0XcWpiA9-3wZLCN~E14sOcN%kX<Pl=bO{_w$6X+NTn*bxM@wfCgG3PSkl%&2vt9= z_mNaTOJ-k_i*<iJIuf134A~cbO4g7?nPUwnzfzAf-H#v0toDm{nAfuWAXSa{P^z3; zW*~W|+ZI2?p>Bg7<sfq@-J2u<GI7Q~Lbtp;p$fD7)};dz(>$sNofXg(m)&P(NLN13 zQFK~^OGNQL<|X8XrIBi@Jj!}TB7u%hDXV(7o_4mSaa70<=!?HAe?lQ294|XBAUZ5o ztU`^>=&fE0Y>p7y=F)V9)f@zHewm1o>Efr!e@w8JwX5^(W=TxCf%2G<J#Atsi}3U7 z`rk3X+%<X^!P2L6ETnD$4r+@xe*-{37#`(Icg2_A9(6`>6nzOtHkRdgK#v3sZ;k(3 zi-$gIQ%E0BKe}_yhz81`H2y<GBi8>RqRF&U!`7cIV^Y%VD+>R)T_?-KBcJ>Z<2f3M zg)SNmG%(@;OSt)D?XQ<gZCcRg|FBUNU)=89->*x&2STs(be9&GXKR87@8gznx<5Qe z7`_z|5$$AI-21-6?MXK^33)J*^zp-E@6}eVDhb|SBMw)oE1@5nYCr7bnTL6|&i>w* z$vz*sUwZnvY5qDx)pT-OO0SKrRVp!bHDKIlw#VRBef!pGlyomPsM<UMTEicKd)(9a zE$G3n^mXw8X*TZH8@(5okj=p2)oVwNCY(1eKh8|Kbz!`mfas*{J_;A}?&~6><KUi8 zaBJcY17VDj1fh#pJLtyo`UByvaNJFLO1-##0L`|MBrOYbA@*|0#F9IDN+OTr$!ijU z&L(V5yOtF9U$>f3$uFboUw@=C)d_Ipzont3`de)QGg<rzG+{I+)9IDCJ2E8<&j)(W z`t!;NHOFDB_=0vb%Sd%uBJoZkgZhpK19q8HA5!%1Json@C_6M2HD7(b4om}y6kDv; zNq*+3j6695W{zbVE)MeJPQlPtjp3H+_UcA|;4r5>S8H9CZ#lA`Q16@QDFM1~KQ$XX zlM;9IgPyZHG*pLa{COQrAXxb}_dN;mfP=Ao&hOsLw1?3Fcm>d!9T=`5ux&**f8x1U z1$l^)bn7=$lEE-StBBh!-FaZBt5I0+M7NTZbYF3vJ*xnsQ;8e#Iu`BU39xG-SKa8_ z)tn%l^f(?h{{y6Wy!K3o;(P<ce+Wwq3D4pjm1b$O!^A+J?_Q-HM^()_l}o}*Ootdf zZA2v$bfRG+rSe(Z-`Q2a>`U(L1pRUtZ_3S>dv*yIR-Rk17Z|l<A9Z?9R)K)oYbv`L zGa?<vCl6jT9cAm>9tHdX`<8wzn%Vuj3B@MVw+LW$Yq|}1aE%gv<0m9~_O1SXZHvHA zr6BbVCII+l{5l!B)k_ebbHs-;wO6yX8M9OSkl3>mrsSQSH|2;E1JWSTAg+FkIOOS* zFG5(M@O#5MkqX}XQT<7#HQ?+uj5d3Lyu{OiX-U1PSG-Rc{kzIr+&If-z5^=5L=U~E zwPhl)?Ov;2s&>pCi~SY>>9{i$%w~ghj<9;sZ5&SU=@N+;hiQtVwhdiaPN(K<y@zV} zS9|<hTeG!7R0~W1`@Q`Zb<3T9uvl!(E!QgS=D(EG3I_h~#J+gj%-36%G*JF_v0ZHo zgS+@qeW`D2;Wlxr{%98xafDd_m;6NOjAT#raYh$%38>hfvXoIwBtVMw*v_*VxCzb{ z3zySoEj%D;pxG>VQ=AFio;!SL^8B{|5GyLi5(_&iJ9`ffDRG5v>^6>EFBJD%O=it! zFk4CU$ND`zxqs@x0Isy4`3FMetwHyvyhv9HaZfI@QA4eg&|81=ZwT89XGRTD0W|)u z_J7@BhopJ}&&Az@A6d@_h?5RiNi#Tdv@*m+tv%6ZX#H)%+p_ws$dPwbfEny?JkAsF zfpEpr_;InC*~=-xOXrK}^}$rcOT8m6Ma`ApRL9Q}lVG;%S1_}+_XS_5yzl(_*md4d z6MSarPItWHQ?5`qk>uj#7J%&^tUT&j%{S8Ua@^5j%xt0`RmzNZ@3E5;#YawbE0$P& zqO@hGb@ueqgT8<*HjIC9mH3>l)OVgAi3eV6q^`9bFK8d}ZAxjq_e^n2h*u`ltf8Zs zZrz7-?65>leTF?IF+y}T+gbkmo@zmduIS0|9%uYEL45OFIh(jYhx?E0y7#Ep!~k9p ze>_8BO7FhnUY@izpeh!^h;Ov>(GXa~c-vI_!R!m<d<bM*oZ|W}zpV#xUF|(BKW)$% zaM&(l|GmLlbfa4NG2clwa2S`aQRFYPJ<D^<IsY&Rhhh^hkMy9jGKEULjs=XZuUfNO z4)IMcq|t9>1STo?8E}T>aUKtiR$q^WDhO`MA=?(I4&9^FNB`#e(e3H1-@xa(C9zYt zZKEcZhk4yz>?~`Gl{d--^SmvA1eRY5B_Kx5tywT$&1~xZSjSYV8+!n9`8c+cCtX-x zGM9u}m?!O%U5>YZ=}h~GHCcaB&ymgX1nw<lUtUg1l(u%AU-}+i_f?vOEJo<Sr*CZ> zHd?*AN5F+|wWrINVM0=yUgq#*$ZD8*q>j`i+Ov4bGmz=?rED_W%2C3(tN1OKLR?Y4 z7g$uf^(2)^^EeHd5mc_Ww0gX^WGuwrLnh}udHR)eAbj3Eq@^ep9zyk{jpdVA-&t{4 z`@zpfou=_8G5NU5;z;SADq(U7(T9sEtY!HM#vH#^bHPOTba9K7hNpM*xs)I~<9Vh^ z1w;|jSUrW_8QS1sA7D>%voOh5kQ`?{6&$Bq8=EW`Z2Cgu>Iy!WCsuN9#|Hx*^YrJ8 z#OSP3sbT<GI`IxD;Y*T^R!3iz@nx>Vy$Lh_Ao~+*A5?s*Q|D8pE}i&N2YM#6Ce(ph z)4g_{W(_$Yg!P@4A)gf?-DE(PxzBbyun<smyf}rX2E%a|nGU5x^sA8h!)cvT=VIGN za5+=yBlh?&$}Up(F<PJ18mKL&@q0A=oZqes`+j69rsdJ3IQOB(9Fx>4MYwF{b+6k# zoXvdpEnZ!!$Er53cD!)}+RfEG>&WtOl#cYQz=`Z{?>Z!jtt7G?pSi{D&Ya;}h5ehL zTuI~YDr+N?p!On{r}UPMyg85f=gJ&jePwmy`A}HAVFjFBiw5Q#EP{_oZ_n)H{w#2O z5;!l450}J0E$Ol^5Z+&O^V<`W-`PJWUGfW9!%<F8<~0oZuoF;ROmKpR1AwPy(~@|| zE=YR4wOO4zdiq3Sa?M4mzmPs|T#;IoM=zvIl)A_5ex7+%h%Lkf>8K#Zb&^NC1Xq(F zpZYPTNX;{?ms<xWK$mgHn{r`8Y;c}l582+$Mc>%w3t34>=Q~y2$s`ve@F@FeEdNGX zoRBfiR;=Ts5L(SfBq*%Ju$D1gDHOx&mujep6a|C9V)Q=4oDv}_*IzApofHY2@RZp% zT>Si^rgd5FkltxbXYNW+&ihH5*)Ar^<&B2B{~25V^WmA3A}oDD1|03VKg>lxq#j!p zeo328L%p`m^=ck3E)MCA*huL_y#4`s2pe|;?Vg?%CMml9`rZ5|7<HWYsg-b9iLrbt z#b8FqS(F+(q@~8;Jh!=yt^ibIqj}$>@&(*SsPirCO!YJq;^6b6jlo~s+93cTI8{<t z$xp=+A5jKoKFyC2f6~FL4_~%C%C&j=ByNr5jS~Nkyy2HQ{#9M}ZEl{I1#-tP2z-hD z7(aOFTySwym<8-p??GVCwfvXxKgf7Slz&t<knjL&a>6mUsYGMy=Dr-0__=X?sS>jI z&Kv$(^t>490XhCFcv|Mm{lJ>pW63Phx#s$@$&V{vJ^+E(N9F|m8@-{zOr^-mvl5ta z{ERC2VvoJh*Iq_k((K?$|MP}!q?US0|BopD_kW`NBKWEwLn;g<on$^)b0eu^`f%`( zb4CL0XwrEE5MkG6-(aph5OhVF_!&#YWXRQe)ea=u>9b+y8yATu8D?C$?L%+!s)dsr zhJ_Qz*L~@9w9j+3UyGZQDLe-=)SjGoYHpg(k$dY<9o@W3P1H2k_UF1YtMt`#w)R3X z8Zg7Md`IsGQYlP#UqRgLd14z*%KH(s3Uc5c1y9p10b58+V7a&F&9rXQp?LzN=11Di z+XNd?0bBO@sTHW=H1sqtC#Yd$?3aCtA%!DQGLL)%bBDyvFnZh+&jISYS%VA%?_0;s z+gB#-GdqNL{m@-WHZ@}czCAdh+ZILA-@H~q%&YvrPA;ANTv8Er3_kPRl}*qllhYxX z9s$qx@dPlXaHEkqDP?Q({gKXwr2nz!Dy|O=T59po;$Ws4+*Y(>@tu<Ggj48V=SCcg zQqB8IkKH{zJs+w-5t~-00n-$sRZmt{it&cBx%MI7`lO$~z5EM86KT+!?Pbv~4#|e1 z+p2=5v?nwoBA188Zc|(jaL|o<@GW4+(j<2bYuhN3{&slVFasW<+OeNLINdWfC*h#6 zN0cuV-(T6_g8BySr}XPg8GMplO!N}1{bhNceVDD@HPx8^B;wDy&(Nh##xz;RrNhh> z(MZTc$IX!+mt!dp-=#fV4r+O$-ot&)`w#R^KJBvfk|-aR^;*tGaSPUdQV>>GA<BCB z59E-A{H^~CZk~mE@u3v*toi)uOWJ^gz{7@U_yon4-qK82^HSl-X;Fs7#MiFh0bMsi zx4vODyT;ez>gwu2@ZxpWZMTT7$<T|XW6F`82kLwOK*>eW8KX8Y==lQsnL<I(S3q%F zBzTF;&$65Rn)f;@HD&>Ij;J+GQmVPOf(k<7)p1yZrpHNvFB^1I<C-tpW`*3;c&~Tk ztRv<VE?OqD*X_sLHWm|X2n^Y8-8`jF=1O_{?lnTO+7~U`>63}xuMI75wGtUdQP*n7 zduu+{x_4enCQOfBf(OSF-c*u8%m9wy$u(P+GgtQlF9}I6YG{ue`F%?JmxtO*UJ=M* ze{4Fb>|#krrUTz#YrvoQS0!&Osn=ca3T?61tvX(*BY#u8>7|#E0LFB)UP)<Ip151% zL$)y21G(!S>Nt%c->v{6J;{N#xI#zGjxBp*dvRIkVFmDYK#<Yokw@+_73zBF)^}6k zE61`*X28Q2XD`z?A&C+sI9ugv6-al+`4Zse`5Dhck75<X0m1~c3TR~p1RJrTCNB6f z6&eBD$>@8rY5ILmaYO>TUyf_>45W!r)&GpDc-QBSocA~%a{ULI_G<!oO*p`QVrveD zqlzjEW0x;K(RuZ;x2V_Omm-yEt{WXs?_?Z(<a?N4c*yom``u9=I|%0~boM=}PqhIr zK*F{~{&Boyr<s;}-`b^Kq=WUJxSwO6E>%31^-R`OmLV@B(NW_Gs<JeM3iueqBqJR; zcAs0$mrwX?1DJRDmU~@|>uY_VAK7Odd^U7#4jf;)miqsAddsl3+Adl*6fG1eEl?y7 zind5`mjEqRthh_j;_ecpxCAK<!QI`ZxNCqGhakn>A@9kz_c`ZpuKY@_^{nxXdCxJt zDwf!~mrU%ApA2D1`|O31zW8P#lbB=PkY<Ax(K1K~^2ddX1C-F={;vBYCGrj2wWWjK zBUGGpOX2*W%qA{!Pbpc0^(`QAHbr{QZZgHm)*z_!)O1p$VanPK7H_p{`$~3FDDtt2 zTI0-1jE<ryrS+C&{ai*&P^~m7gsE*2m2xao(>?317s9#cG(|nw)(>wuWd-aTdN>p< zNz$rro1YPE{_{J`4P96!`vf_yox7|KQML5Vi5*;4pL0Yr!yxF;{HfcN5g+k3crFq_ z(_E*D19^g=%+m5G{9^S)lpZh2ey(fSwXnxIeycMV{F)xRTxb}jGv^-MDJ|@zFC=BH z{4nYAA3zkfvjwdDfE*?cctWvxkh?a)h?^h(=;s@5+r5b3c2Wp(6w+^}+Y=yx-K{h~ zncFB=IOf|+@~{6Ee5`){9FdQ}J_jSD28N^f+d`siZB-JKZ(W{Q21E=X)^(`X`k4A# zC&R>2>m`Ehq|Vk@#*L8G<QoA%`lW&WW#2<W{n7-mGDzEO??8gQT8n0YSulw?WuAxR zU+cGo=FgghZ*(BN-SE83qmLNV(XsMEaXDH+o<14@F<>081d#G6?!K>-X!Fh2WsG%< z>`uR&OrcN234&zgH`i5063B7<qL9DHI-uzRvp_jP&HcQukY;4kVnOFA{9g~RNE3-e zc~tk9^7OulOsORAu{a6%{FdhUP=n1)AYv1TshBQNOXSOv<rBS7WQ%@Ot4p?ix_R-9 zg~zA$B9@*32!((3CEr;0>PJW@IDx&YAVi$m4_}_KB*66h{-FYw)#!3f*E$=^L~#IF z$rz+wd*!$`t;9O&Q^qGX`qyA?)VsV=woxFVkCyx5yR<U-llOvs$pYyO;rpI%Xd50! zia-47`u{SekO>ZQWz|0akYpJs_1)1FBfz<DS3mYI<ofn%!|j<Fhy6%4V$_aRkkTnw zD`uc*nlDz#i#kb?`9FYF{BSssP#->4Ci`2*1OW91C$Sjnp<fh(PT(~M?l8;b+GarJ z@mBgunysFqstk?S=lIx0t45~7ohos#Zj2kAu?MDz4{oZp%4_ZSPvYg=_au61*^aav z0N}$`PKsKJB~htpn0GT{DYj%7aBu&cSourUvY-A2KaAt!-Aa^*<H>$ghjo63Sm_7% ze-^#toi;eAEi=wH&UCh+eAK1YzL%k!hI7rNSo%3XWWoJa2Dt=yTk(%KRDSLwM|dZf zY>FSP&53+lRp3DH#cyuL6T#Fk&KF52-j(K)tmO>IL5m;hFSVo=@}r4G{sYvi)6Mgh z{ZxPnGLd+TD#;04JUpb&kdL4xs<LEdwb_=PEH(Z9DH1!TYpIqX7LOKy<y8c+86Pef zh<>&{vUGQu6|3|5qa`Y)q7h|7Hn;#jBhphnXtrJcLPNbmQLq%VPtR%~^i-4~US&4& z+~F<lv=S#!hR@idolxKs*i)J2#wMOs^a;GmE2;M^L;f07R$i1_pxUahT#-1_a-&<5 zXu~QlxPPpUzgyxh6O9VlvRb#Ff4wcO1o>Oa+TE?a^mq05O<62&sNe|oJ@HiYK|WFk zrhE%|xW+R)q<^YK+I;>@v0uRr6vS@ADf!DAB<S$uscx~Sy?5(3>6vT3K!BzyV~>f0 z7r$)X@5Hp(R>{^>-D09Zc-vYQKT;7pbU|LOlI0+%j}*x3Z%?Nk(g0s9{s*8g=iGmY z=&>4^DyPWLLg{;=DXIZ&sRh}|Bcc=LaNh58*Q6^G95jwt2%U%6q>|@k?b*$gaV@ZC zY;ew2QIM!nI}EZ!b>OGDtY#<L)WS+qd+9DhlrBa5Bw9SW@a;YYqYiz9RoZ1m<7Vb0 zq(5M|oxwH$)$a9bt8|}qDO&U9pEl4^z#d0%1IgR&fqBti_YM~e?fwxa@ZcwyTZxs? zI@uKOj%o-1VQjdK9M2sFYO-D}XS0|kVi_xW!?R7hQ-pU^ostAay!ZaVova?hjVYYk zOVJ|G_h)};7Rud2{g{nAM#WL{2_T&4hdI_JAKC~yO70ja3SHV8s=xzBP9^uXR9_n! z{22jQM$fk-Lb%$0R5ML}5^^P8Ka$jA;}kJ|AyEG2N6HuDQEb~cGP>%s49dSIg72~q zzphERtwf4ap)p&Ccnp0%Rf}Q3*|K=zOjL-H_T?g%=02UfEbXo%k(uD!NBYb*$7Ob) zu;w~iJg5LHOhh&2(>in=nC&FXV?1*%ri@MRq@+mplArwdp!-vf>2=x81;f;MGikj2 zJ8Gw%p~9~Z2D!ot{e&gCJj2?Tg48SOF@<`!zvAyS^duds_WqlDA_mS(TSU)HU_?j} zYkY|>{(r@H`v2XC)xLgWDLqq0Wo8>`{;z~Ybw$|}US&E?ZLza;O6hXxCW4sFE+l@7 z>b$%DGvpFK@Y$vJa`D(`A>p^qj(85toQXP(WO}Ktj>AvcY8EJ#Xe)EedH;r4u63*Y zpKWD<-i9$i2CSyJ4P0=`ZZp7H`+UTfbV8i1`#|{H;{|1^_=eMiR>+@Ei<;H?XDdGy z(SWYizn>JKjeT<5D5XINaigE=@_mCp(ct5aGBuf>YMx7MsX-CSAMAcMWYwc;X-m6b zt@@7;K-z3Bm2cW&L3hfC71{gDAc13}pgVGnIfT`$o<R5bGiYG`SinEuPY|^08|CeS z=TCwPA#`J$QW$!=K(Uth$emY_qn+odLbv|_p*Kg#q-@Z30MeqnT|L1a9GfL5a9Ih} z2n<>ak_On@l*VeZz7qs!(X}q^k)0ZdfIQG=t5y{dj7d>k)cDRz`A!pVue#sU-&AR- z8)GX>{{c??3tx7KyGW%wkR@4r0>j_e2nBwSOUk2z1u!`Bzt?4qX?;cPa!s-iCOkeO zya{N-yJ&dt0d%TaG<=JYq2@#$uRp*~vBUGRQ=wZ{GU1C%w@jDi+2%7X0#VrzWT%Kk z-Ychfdxq`ZE(3G5pOJ4kGw;EbdK4&wo_zC(TP^S3_W;^Y)+341d(@gJx(qQ_D<>i; zZbFNGUIJ;}VNZPhbST9*a|^mQO2Tu?&qR!E{0DGPyk=J#bUMdCojmM&-^QwyYa?R3 zLAvlq!vrp+smI-1)ROquCGq83<ujD7D>2MD{<A0D?$ia8?skM-QyzL4{f+JAJ@E6h zv3!(Qcq4J^zlo%%cqq!`*(bEk-n=cod^db?S-r;pRBtpq2WfU_`)M@1&~78-ppyN# zo9b36fKurbTH{mOZV#lD-b?zd>SiZM&`pfkqakc|>!W3F)d)CD^y|%`2G6RuO_Wq~ zZS)Ws6F0}T;8yhzLo~lr!{Z56(%-Z}Y3UVXe_k-R)W7d5o>BU6k;b{>C4uosj9Er3 zA7gKSH{rsE)S@<vmS3X*$@-sO{=;-_)*WckuBq%VeL4`Xw0J>#Px<T&5z_Ub>WGAT zm4-!Q(x9X6bk?M8^SZ}e71F)_r!AF&yd@q(;me`_Gn7Gk&||Cc<r990lVr1_NB#6( zhl*2$7ZDdWeH*lO>a+e(+7|aL|5J#&Km8)9K;F37t_C_RQ{!!QzfivnLXB-m=|&I* z`@ZBvVn2Uu_f>#|pzLV_qnm}zq^RqX?j|FoCv-Ai(~@)hF7?LgE>gM9GZh5`BOgfS zi>jHxt!nYHitpzwP*YRMpbF=1lXL!Z$ht{;H=eGIDw(R5`A}Lr{>Z-m>O!~B&Kus_ zLj(R0-sNgBV(mc#Gm$+&5I9TV<jbjPLq|hbI+vJfV1qf-9`Z_+0an-wY~7!lP&2UR zcb4EIl9O}_a7u9g5v<ahz+;!;iQ#mp1s^?fk?;l>^fAG7>rN`%UN~z5vrjd1+uK%e zxhKVr%~lPKB3_i82l=0RyH9HgmR7K*y9%Gj^qu<PUNzOWqaa?~K`sPRO#AwO-u7;a z)>1f(0Y`={J7wQ6kzq2&7u!rSYu!q;d%bG>&1~_V(JqvU|LzQRXk<rS#HSVfF?fzt zzr@nqfvTaZ8B<8|O$!XRU#}8&e)sgdmYd8Y2IiNW0yk)?sufx~S3H13gDj^`YFVGi za+2pVvsV(H31t87Y6ur}Q+C7uy_0Wcvz52py`_S7+I~uM%oiR$MqP>`%iic4n*2HG zSzU=f!#8}#Z$`N0pV#$b36^g^@W*vIIT4=`iyQUDs7f-|kysBw#NiMUtWzWJ5!gEE z1gB^P-r6MSXW}+mC{74mjkGy{_mG&HSXo>}8~t&js26>D3O{y0KWM0vMc?39qzMeV zjO6*JnC6TxCf(v{@pukZ%6GSBQy!hYT5L<|G3FaA2J!rK6mmH|*09@ceV371iyQUl z@llHj@!O3zB;mvy>*{T0;J7_Dy*C`5!m7voORy+!lHg`+Dt-@U;YAh2O<j<_N6{;A zLed{iNEbQXUf&^zx`gZQt@R#_zKnYXQj=$>WcEDJplkba&8eako%cvpRqsR^pHH^s zknyd_$8Q|#1x&A5-%+fw?+1lqD}~dVr)TXPTw{uI{}x?(7Bv3*P3DD9rOMf0hG6V~ z&Tm>-r^UzKuDyO;{;iMg>**YjduB0*u%hm|H(wWAp8n3*wAt03Qy0m@92ibSt>x{6 zX0Xr`oDMbII=R%g3}hREu`Y~^)YNpXpg}ur(FB4w)d2%c$9t#<Q|^WTPml9VB;`x< zXPU0(Jbm1^vY@Ao1?UrITn|XH;sD-Z2ggU2yA>w{Bp+d;cN)9bHI?H3Oip+SqN5>M zkI(AS{2kQ)rNyb20PV!{nNz>1xX6C4Fj7u-WwaGwDQe?CJIbWKw4KeG*hBTO$MasF zt<rTj0u<Zf`?p+Dm_vHC-Dy&;;?YL<M_9^kW`a|;|9n}xFNWo~2n5yc%Q&3zD6);8 z6S$I|Ogu;Tw<Q<gsLV@L)2yFxe-B|c`Li4+FWdh%woy(e;f5za@#|{m*VjW$O4iGY zMP=J|%P>b4Mp2>R4f-r0-&M}#LOne_44QZMvZ#EghdHhH1+upkra(Y<O68YD3SFr< z;%4;sR*Yc^nz(w@kL~lOr0I0nVjYPZj<Wd>5955RgZ}uCf0*WM$IxFJGSf~WREj@P z+Xxn%OBDoYlgcFVWHm2oOai4joRwJk#$>1FsvCCa_aw+xaE6(=4Ze7|w<9;nZb(M< zEu{@~H#R+B(k~BQf=1i!Vl!f+rmUk<;4RHfzBCH)Qvi*A_-1r~sl}8;0CR`xexx^h zHwjNly2XoQP?EaN8$&7hq8&V*_H{m)h}zCVj`#~nJrOq&LzYU0Fo{fvd(x@ve9sag z)r*DF{d`U*gUI8G_kLs4&feRQ>jU%(OST;!w{pJ42b)%9*1b7<=DU^}Z8_~TG{IqF zqj4Ws!L)0Ira7XgLq<DYL-&EhwKn-IY4|I81;7<LN$OHe31vqe7&kQRLa=$k5}1l5 z)=0E)!rlLw%5%bAu}sfBJbScmdp=J;kf_umYP#ZKVA3<}9?qoM@sAc1Uo6&=A$QTO zTjkc_Ogn{;-v!6zYjlnDgL8}V<-hejFCvAk>uu^x@dyPcx#gbQKmLA=xXk+}CY`>1 zP=TiH5}R*TuCi`Dgw)x9E!&7uS!|$Uesjx5$_=*t_nLFd-d^#j?rGCGLjMC$mB)=# zib?lDZ>3Zyk_+x$tOp`TDuw;z)jkk^j)sjw487I9yvu`X(xAsV{bNqfB`tL@IZ<y9 zv5Wp1olN!MdQ<xD+k9$W8;`k9$>aKFWMQd+^^h_f?e|G7N`C{CVY9FgYe3@*cQdN( z#UnKq{3nfeI4M{uJX3N{XY)Pnm!-&t+wsFqnK#G$WH$AmUVe!<4rd>Y&ZWL`L4B#1 zeh8_Iv5>!D!HgCcWXtlVnm(Bv0L=zGUTPW+K7p=Vr%d|K(?fR}-IL^KoNgtsvrYV% z7Zb~6BQ;uSjDs)*e!>dFD;i&9$}g@LGV53qD4C0xnL3dG9S>@A)!v&BR_a__GklYe zPMn%hmf+J&9=vKp-kqN)%HQBMZ785!)6aEHm4fj;Fu^H|YvEzk?ub}_6HK0t5C~`X z-^Gu&MNjolmr4{;ecR*#qHXD3a}Ia_a|G~0@%@~AAp0HwOp)P*&7VduBr=|p)^pq= zvQ;gUml!<}BupT4sJ<xQWxi6sC$=|R*gSi3e`$|qdov80tn?;g+SQ|A4rSXlj$d57 zE|rd_{@rC7FS-9o&<drPR0`;*dJ)vcPwC}eZ`RXL`HjSa`8-bhauR4LkzVk=z@sYv zx6%JkU5uyiKcO<k|8Kx}DchGRg;x4#0?4fLWog6EmtVm6{ytIRNb{+efs4m7_wXfw ziQb_0P{!fwZO!4|u-eUgjC2Jh;d<1I)N`TSS-c_(9Vzi60Q1iYhp<cbp{^yX4N#`X z^}9ch-CqK<YK60PHv6@$j^4kcN$87(-pwJ2wCiH?cTnnC=RP@5r=xm`h^=4W*@45S ziI<|p{}bhF&*di@E$0nNJsfL@#Z0Y#9>Kt(<Vk^LO3;YS#U)~)m|;CCpZ?vhe&99l zT}qHP*4<#G`Hh;9-_(q+K}Bw#y{P%v9i$INQ`&m-u+^1&MlX+_)!@g81A?j<aWZiY zf|vB4xmfnIF_oYsb+KJ~S~sp45B;jg57{H|%Il9t_ubMSwZ<!NPYr-9)3|$VPB*b$ z+lz|iD90N+HUjvHNFP=n0RDQAdM4$p+pKcU%i7?)6x?9b>IwevY%}LeQYe;d<su{? zS?^^@m)OLE3Vdr<wRo@AYKBUyf#J7yhtBTUdD!yPC%hB6w=(bqZ_$NOLM)Cv5Ph>j zVBhYfudbxHP6AZOW7T)*gA(_5s6#V-_K+QVo#n@}Sa;+_ns{I(v-!j!=J-@L`U6)B zpM78FBV&_~+`gu|4Qs2IC2Hxf;fN>OMF~tO737P;+tvDJ2t;<LX3K_1YepdpE~heo z-TWUfr@VFDnWtK9|8a!U3=f@=0Mkior7@QS{FvkG=feaY21*mOt#kDf1i%A|?6gKD z_ylvlU}49wmXc+uItrVh3Ha@88R%~t?9LeyfU2M5Y^aoISfmVfhD5EEZ{xvtCc}ye zw;Uc1GM;0KZkdn|6;nrwpYO6Dr}>J=-p*56q(xfx__YC{;#{Iagv16ZlKmz|KY2*$ zQ6I(rWDlG)hUNZP#4GHjkU}_~KBI~uXLk9W2s!*l!0GVi;TgSF`;$3^7wqlGR&dl> zhMPkX+PhU$vs2ddLFGaWBkg$bvo%D>d@ZKY)Z!O_K>?P!8+Uc`Lyq(pwb<KJ;3aaT ztisjBnU5?&gJxq7n=W$4&5SFXadM*sV^>^I#1#6~KP2~OIEW`f=h6E(??|znV(q;j z-B`Fl)UyfFv*K^@%n+#ksm#DbG9oubq4@cSs90kfUl^9NnZxX_W+0oX^xU_Zd|{Bq z`mML->_{R1crK18jxJ*EOVcW{w)^Y93Z6sH9@QhQgAa=)4Lm=W9FZsx+;^U`QcFQx zh4UIu>l3Tpi(^eXQ7@7ZzKr5q)SS*w%>yrA%+E*(<Ei`Z*j0>rflZw*hvr4U(+Rge zyv-tQwfp?w-xVGkKT*yg7Sh8hNQxq!h3dh&X2OoXq(Yu^c}U5+Lbv7(Gcf`Ltu&ZB zZaBBOB%w>d^0CO}w90eruOg0>yDBpnW@D@*q|c=jK+M*Dx(=5=Ru{3HZ7lTT!(4gc z^v9#(;|WhSKVCCToac0x)F9UVP*=r`j58Z?YMA^`2YsR40<E0$_z&RUaj<2;GOizf znXPPUm_)aa=y7KcS+dW-2AwY26Iy3)LefPtml1s^+j8JucK8fdH2#pts<YV*WS&3f z*F`0=QP?<K)R?QvGEXehAUgHVqy{tC<|j&)$hVTtWBcr!@w`m)J<|$gSM&U4Mr!eZ zZdBNn{xOlgU#qdd@5vvHDn4ybF~+`!*e~Xb)y8xoW-JyAIx^9MRy}+`HwUis=1eHA z9tuJ-r`<e$^v!17#@_p5`h2iu+u>{9cssil${)etxro-x*XxB%Z-%OVzW;R~<8$Ii zQOG}iB~dm>(iDw2@J8}<FL?+K{|`wDEN5{e!oZmlaWA4Q#Qu4Bg_@k@@sNs2?u6e9 z{s(aV4{*+Fylz1K#eW_33)K|J|LW-_9EY6qRC-+6YcGOP(Ovy>M6a|w)~y^k0WWw# zF11Y0k$b*APQDNHE?-(MEMr>q9i6*^7uw#t`_DDdMh|va9v-V4c<@!5av7C1HYe+f z%RQD)*&|`s+%a1--+b#8&OyhI*{pcgN#}ib8}pQUV~2<R+I8wR7=erFz8(~m{ZjJ< zr!=e@X=(X%%`4kJ+Z3UrH+u4g$)ihT@VCLc!+vdCkrf$9h>w-mqCjQ-I*X}j<SJiS z@#s$Iaw~B(K?Z6%$<hZ}690>}n#~8={c!GH>AGZ}8bL<6WQ9XM_%0JJMloOPaCFNV znD!lI5ry~-fwWX7uUDnB{sWv*B1`Va6QV-3ANj%K`;Av{@Fs1yLk8A~8r&P+6mLyS zD`xVNi9YZ*FIH#i3Zz%6Bm`ydjAO}kGBoZi503UkU*KS`b>Weo8ahlMEz48bZ-l^` z(Cr`L4s{%VQSsFWEC@19Q`8jGy1euFW?mb<?J`%XnSA?=#$J-j6U?r#$Susze89&q zqZD_a&G9k17G3aAmqIG$pS$YcmEhJP##%S|8090@ukk5lh$<E%#g_Tkp~liX!0hdh z_-g~Vv%5NQQt~Mg*lu;@&l0aVXJy{hS!)M&K!6f=Pmw`W&RB3ubS8rQ2AuUH4HI7L zXke%^N8RwbaQHWB^H1np=O*qzmAiI$7-+w*8Pj-SYICNCfB7x-9b1D7j+Myx_Jc_w zRBmJ>MbP#_*xwD%)cpl}d_|`8Sd}g59GbkHZTK-4timw&TIek!*6Czndwo4ocyyRM zArL*%)T)__>&4waAsF&n@=-a<JqE$D&crMEaiUlclg>-z_Nr{Gc<HAF*Bcxrj95-2 zk7BVx4}V(V%H_ABL`E(KbWwpR@4#v`+C{~>Y;~C0dvYodg0y1;SH5IRz_2uUsY+~0 z9$Tw5Zlr-|njk<+jx;k@LWeurqG*wIJ#wiJBW$YG+AB=SG)N13t?YAgUn~RaQ8wUP zA$pwkxhea?wZ)kDru1H7AHK(H^-kJY5B+??k}u`Qf$s?C3C@HvR+=S6%q42J(b41Y zW|b!Q@{JXqV-F@aRMmP@$b^pYS8ONYPNc27koBmZBS!3TqC7)#8auV|Vf0CvLPEV* z@~AcqBJ;QmUo5#(L;OH#jZ@2TN<pDI9p~_NLes{8=2+8QkPMiB`Kr)VOzx^SS^EJ} zuk-F*o9DL7ogK-HJi)KDrSd5yMRUdH=#5%j!ov6Yu#|7L<S)Qat{RN<m4Kd*P9Zei zIjZ;CPi3M4Wva?WG{aKhGm2zgLuuVZ(#KSs5t_@M0^~}JCy~FKV8<L$l$Z5OfT^AM zQc*hLL)U+RRT2j6Zi2R!-d>2%G}Q2W^ylosy1WI}cL1r(s}0Qp-H94<Z8>m=@O|83 zd|unpc>5QTN8s##0Q(h3QUX7(JW+eX&K)3<tQ6c#=W@~ON!9(Ve!9!z^|q?sx1#|F z+f;%iaqD)&^^{ZfdfOvU4GC!gN@BmR77AGU4MY#vj8xi5*R3w#9I9mIb?ULHBDfwT zQR(<Mg$$X(b2fpAAtDcMTh&rdr5&NP(Ir(bNe6-={@|~>JQLREv@r{;PIDNyr+RIE z*H5|Ip0WG@td}UeVWmLEkFg@5pq!irAlrlD!=F^GD=F|ZB=!WT4k~xnN^m&8pn{1; zAwnffp)KLWdWmxYCnceX3`S9%UuADvP^l%C&mUn*6@Rfym;`8Z8{6l%O?-<!&t>|D zR?6^|4FpK5B~D^$Bp9D?+FpzP!3dHu=<8(vdu{yEiU8Yqm3awY;};_Mii8}hBZzr- zwUy1xcvMMMRs3EskxX<s9fjU8*;^iXn{8zgdzZBb+>=krT_b<p)6R3@j>N3WT4xX- z&Cd7V9a59}x*SMb_fwd!pDT&_T<YvQlyjehL8osu6XsOR-xvRWNYpa>>~B6-q<^97 z%Z~UKLn`CIJ&Ar!5ArU3hPq!w7`kM-jtuQ+Vx@UCTeU1-O<=5#|2O~iHZPLpGe@Zg z0hhJ*`%6mfoNf56Q@ECOCe$#Es(ik0gTA!w@-NwF+3q*Jv}iAsqGv)f`>_9?k|5oK zj<^3?9!vS(LcXE-b!+oy_$NUC=>7pbSu{InPyBJv_vkWqE4Ky2^xToXCtqC3RVB1* zw9BHF1*^{i-KEr^P9NQ8pEDNiXp`CwfpOQHn6`qS{>BuGCOww3byZmv4q>xeJhl;D z^WKY%p5}b1*pC`pLRJ%_@hg$HMg*_qaKLLMcc(zq=6V0~VnAa9wCd^Og{S{!=X)2_ zn0|2^GSIr0G31=bAxxq45=Brh;$wDH<F3i=J1|{T4`oLvG=qpfk<c?QFE{yUZLhtM zkQW_NtgL*<Hb<CC*LlA-FuDi!o@hEt<FX%kqS}Om&M*RxD|SGaBi+EahG-=%8i@}2 zAD-H@+U^MWqLgcYx)BjQEM6zEdl!S_pSlgt^N*4jQ6|n}#Z5y<`)-@;piHCl4G3~x zRVIeTQeOxaWs`u%$yUi|>Tt);w%yAT!LEm_wdvoc$6pKT4fUU=^!x_;yB&$)z$=e< zTIt*vV7t-+rx!US2#N@hO@z#CnC?wBny!xik14iEyr}ZPks&}8OFib(=P2rKkJ3eO zxG|=dso5Mf+;jtHN?AAKNJvv=WvX30Z%P(jK;dDx(@vRAX~6CL#l;sHf$LK2a51B6 zgJ)|Mg4p5$FT`~@`Xhsuu<6DIZkA6{u?hX+i1PJkrnPjpeeQnKb6!8jnchq<uLqTz ztPd_1misc}h4zD=59gu@*_0r|B?O(0-+lWF;v8ih`Z{eoZ&C@w4Ab^CmcPwuPy!r_ zbBa3U{vNpm>veERzF>c-^Hx{2c{ilsj?106yw(V5biMX;6v{<XL|jJ^6$t~JP@h6m z7jt*^bl_9Mkn=f3Bz#_33T3WaXSojnS^PCXH_ydp)*c4q*GKN@_$NgV`t+ew6Hj8h zRdP<H8N)S{C;vmO+ZbE%tl~L7Po6&G%=_sLG%fj_3ribxv~mPQZyyi(VUu8$-TdKY znUE$^wc-!a`34ikcYJ`=ZsAKcF?Toy$K{va@-%m`C0{6WnK6OfBsK`Ys}qJR|LE4` zAz7E~T19~(kjtuksW6=jEn(y(N@=;b&TYLrMvW(mW_bUu5ry}Rq3XhwC0L*fC`dPS zouPiW6on!SZS|>tavTDkY9`lLu6mK)MY}LREVxb9dK-lg?Wi6*BYa-V6j_^h-6BQx zZXPtAe((#dK$b)_6)3VUo{lna5QREIlR4h+QQ(7B!OMENhOs8#Fqm5>;oq%(FwN5h z$H7t>0(E)svgYrcr$>^4LcR&#GLPlyQ`bQk+KdSPIhTf$i;vj*xRH#G@zwZ%cMQN` z*&4L3m;3xLFNaI-Up~Mhuu`X=GDet~=)Ga7MT}6!Yt#2nofF?2j%m?WHnu&kz*+hi z*Q~Kh*yO|c+cM*NF|3?RRT7#a@o|3TRIcGqD<aXd!;>u1vJP@9+85IPwwm+{x9xvF zqabteWa$AO+-@!2R)=FdYXuc;l`EEEFY?l)FcxzT^+fw=B{qiUSc=aA@IYHA+Ia4d zJ}SEt-HiX@Q%2^0fKJ0uqM|aMuBihkh|drSLUyr_N^Cp|QyQ2FwR>HpL?u&x<+?(7 zK@$#2bBGQrGC8{PlipTzx_xbbs<|pQw{pG259aa#+b(ee!_}Y93&Us3lg(}KN5=yk zUk;I1!!K}#h<fj$x!oNT-7l>CD`5ntdVkI_$psEoUmA6@K=Ng9eDw;&`NEo2MfE(u zS#o*nKZ5QZj=wh&(veuveIq*o^V2PMdg`a6pMpCNk5O@34KKf7-~K#FHH8-Xa|<~0 z`ORkY9X%M()#6tEW`Z?v)L>(<IfnJS*Ff@9$)CCt{m-~#xuexU{Y}xHfnf_8{{gma z%9jR*@bXz;L8xx}27uo_(jSIqOpI9SSnW{8p|fSiepH42Yna=LvfcSE4mqrXD5&1d z5S9;LX;;}A?qp(aitnG;WS$Q^7j}hwRQL|#wHHR6><d+T$Tub5<Fjp<jnFZt1g2W9 zD~>Ju-P?+?d4H~3YQ(*Itw&ReVT!8QNLx_k(dW2ORkdUzs5HqBd{DnxD$u%oPME-Z zv#cF>AE@83{`WlR%lBO{-qbDx7j-QOrQnTP4Hy-bVB)=A1DO*I?nW)GCx2`<jf!vq zm6~5xH5-gOP#loDzawq>)N;WlWZIFL-gh=x=s;&Rj^Yq1!d;I$v?R{4rerg8;h%I> zez}G6VBdWU=ts6xbmJ7IE<TujY{uGR0Nws6L+~A1l2})^R&?q5hYBjnn_aCWccl1N z8Xzs2BY0)!4B#ALNU=hs8&y$VhPWF|ARK13#+agH;QaXB<t9zoXI|JQ<5h4V6jja< ziR5`U+V$2!g@>6HLUy3rF?{p+iv2Vw4R@(ASz<t-R((}0dL2u~e04=$w)d?;ok3bj z2<=dt%8G>j8Vfm@Y!J31VzyUG;*7899)=sdgHGi&)M<7glvUeE|B(hrw12A0_mD|R zOoBkKMCVOZZTVAWLM|2@YyFYkUf!NFy7mbFi(9Hz$Y<!+@IzejUe{u0TTx!Teof9n zFGR?m=sxMCiZTCtVC=lU^|IVDHTTHgLaxx<i2!sqD(quNb9%6E0niVd*Xt86Z3lYh zq5yA1Rvq2Grj}0ID<>+AE5>oS6|bbv8AL@L@4#8Q*D$}BHcx0^`6~KyovJK<eLcxx z6>)Z`z&dP>oCDN??`}IamrID5Pn@D*kSXhGH?NX2Not`Dw=yfSveN_t!6QN9j1P95 zo#)+ces}oI<V?B81{kCs+Mo_lw)Kwf_B`o@$?tHoXGMox_@BLOwP7-n54MZ)Eb&lO zddl;mv@kijqZ#_jZjFlD3v8xDSfewU+Z#cDWTV{c$w*QD9rP(+;2^iA$!wQ`$lqrc z0v1v6ou_dEVR*T@zdAVsb;ny>_RoAz6WvD{{!W~OAHnq%o8lsKfhR0<M{3VA0@R(5 zByrb!`Rw)JI|>M`zeRr&seL{dEN9P=<lE986m6+D*R&bSoVXwDJ@J6@=CNkQg?|?K z6Q}x4e5~ZzXm3VwG4o-p6k7CMUdU_N2Wujf$--R92t|L-C@CZIZLo5slGe({;{*fJ zsh+wIc&%+xDVAtY_uJxB=i<jr$9A*W^{3s4y!NJO0z`9jwRI(Z8)$8*JD~itT)ln} z8B(wSIZ`{x2lM+Uz8%)*k)N}PcDtND4gEG3_l9n2##*4E9cV1`u+X6EmVh9_bvF~F zfojySWV1(m<JOYaTY;!Gm8Ug+%^gL(`jQW-)FpKv{hiGFV3L(uO+BDeu^dD!?d<JZ z(pc#kp3d--L+Zc@j*@1}K}(C?<<i)KKs1e0gjS^27vm~Ifzzd=Y{$9Uuwv_R+gmSf z1bUZ?p0|Cz^K_G1xetQ4O`m2t;Kf-hZ6nQ#55MCQ@LqEWfHtr5mXh4;ekMGayd;W- z1z4h!Wbx(tj^{{<sl;Lzx+;ITI2$_Oe=5syXRgp%1jFZP^*f>;^*W{c{11bi#SD|o z?Rja*n_NY<HSV%9J~?yVQ>1u~037kff_5*slM!mR-`yWgNnn=;$M0Ejfwxrcn!jLX zv!@Y}b*3~Xu*(s82v5KUQQ#1<wqWGzs<O$?R0W7`6?wL<((WD8(+PtnFA&{wr@`~C zzwcEpHaACZ^Sq`4_MqRvSv@?~Lq7P(`pRD3ckEQ_cQgTN(k}OAd(v^)8Us-O_Q2U% zy{RiLG9qATq4}qAU<3LA^Q?I5>P3*B_l$`k^uL8Cb|c)ozz1E>z~YlN+z|i#EyqL^ z_G52nw~kMg-gn-H$!>mkRPS)Sos*B_w+MZa0)TL-Sohb{-szhpVwDl0g_ud&#oyjr zOdPLZ-TbJ3Ca3dEshfLKlSXs}wbE#~xeaA;iiei2teBoxq=cYdzGKC<uo$|mcHkHg zn7NTJ9h35T8b8lHyEPOwT!HjtpxTpA96O;WxB4uvjo;TD#E&he_uMdP;Q*Eyvml1( zv7P&v=n(zOV+|9cw0Ygvxhe7R(%WMrVbkg*?rhjD`b8K<RAv$|({frfHi@Jsb%dTZ zz9cU6dvvnBz~O9T!WyqLt<*RFn|VOyGxOFE&>md?vjun|Wv9gMA}D((BF1U+$!Fvl z)4KJ@2GlpM@V5FJ8E708>Ha;%1@pS1{IB)I560E_!a|w0O+w@O6Bsq9BRcR&V@F64 zHW&?3Sgg5oXPf&{z8d1sGU$d7P){*^zzgD-Ey{fde@OTn(;ZN~^B<tt<5|j||8DSy zy+5sf%H$@U*<KN*|CEtrQ#S#7#DU$^p;09ALyP)(=lPG{M!ZN{i*C*bu}`+{-&H(n zuf+Jqo)ht~*Dt=jiV~S>IRk=*7rX90pmL3tnk&9QC%YgqH$yGg7S%aVSY75>C`kal z<b77b9#h+f@T-T3;`4V-Y1J5I@1EEcetWS%m52XFwA>XvI{io{c|I$AcfENBaIT-6 zColW?HKze-t`kFJ{nvc@*DNtGi<TC3iCC_6U0~ADXf*w`47W?0-RJlI+F4pm@F1Cs zYSXjO7$-3(aCqY-V~eOC2sGPKwj-&3IlrSkqv$;pa!onxYU3L424+5jje=*BL)&ZI z)kKcm?s-mJ5t6Nxslz!0S=JiU_Jq_w_U_eLN~~>#Kp9O-UhVVRXOyV8rK=+^n~YX3 zS5o}(uB@`&e>x+VkgpSO560#CT(62{o~tK4E~T>~Z`;$Ietc$q_0ylk^bwaqCGBMy z9(5I@d+6@lwDoukLzvQ)Z?NG`@{F+sM!QL_AL$%nG85`mhE+E&oOnO4HP;AY3+Dgk zGtVwbc}e*kZ$b^M-NZRmuwz$7`TU+ENOtc$*Y=Au^DrXV)!@u2LF$1S6^aHoqV2L| zcAfa(HuQWgcSFVCeT-7I<-(7+hq-a85M$*vm3s|?8_v1bzY?WZd_uPF(->CLCvuz4 z%DXn%DUY9IxJHLhR<$z|b=Ln7c#l1xj8B}iQ}%teQg08o#Qf?X`y$Wpy=|Y$YNp?I z6kik~e@C^~7MA=_zM1#{aY5c!g~`KtcA7B(j}y~=Ea}MK%_qkOj7fRAm2FQG0aKHH zArw$aMJ?@`7jIViqn*?X7OyV)%l6JnuNA3h;yqsYe_t!94m(m3Fuj{T;kB{F?k5e- z;eG2FyI7ZABLX%C2V@UsKL;CwcTx_e^Xs^$!Z(ncPY2*Txj0wlg~re*6Yx%WaSZja zYs3$@w>!sF1WFT)3GFwqS;)L>;+%XgJ|{05QmB{6{wT9)Ko9{WBAZtczkl@mQ2*AZ zwwAi?0DF`>HnmV_e(3!-_4{Z}+#Iw4#%rK4&_~6@KGLqtAn7p`^mQy-wTtZ*{B`|P z+DMs8lcUeKymx}`U2xqkT4KciNbgSREj#f$-2MP(`*jgWr@pwu`XDx#|8B8tsu}rO zkHG6u%?WzjzUYqk|Lsw(?x9%wY82kGlkCwvqaW&SW_*tg<hT+Go9)c*UvfhcDLi`Q zzJoD_3@f8(+pj1t5QTju%l45F=FMAk`bSi}74`M85&lo_=Frn8VekG4lV!wSR)yeD z4etYCSn7jQzX=Bk)x7rgY4!d3wr8f7;!NLWy*vc>>kU#z2b}IM^05O`v~uXQD8X1% zR^sj&X;P_gFjVxRp)TI6RpVZj`(o%sSxJ;77k*`Rygt$DzSN(jLxuolS^TyuEM0%B zd6p9Z7qM))tmJj!k$ljKX$NTp4oY@1Fe~k0ENPZ<B?~uO(qvxPXf92pbM9q-QWr2k z48GU0Pt+d#nF6qk6?n!pJzxa!=u2Fphr|IKwQBd5I`s0Z)!0~_Ww~mpd(PFK1Kd_* zkAGJZIXI79^!E_O2L?uZ>*=Ef#9Rr07ou|D{jIZh#o+}<J+bN~+n~uBaE)aPBW9cf z)l0G!Q7Lbn+BVaEOHkMyoO}OrgPJ5INst`-ESE=^d(8Z-bE0uv+Uw<7|3`Eh@5N~` z>^U{*x^z@^$8dC)(?5Zy31^u4BBg1IYMrD;o!Fu2+{0qYU1My><|-R7)J`srfWtl3 z>u~)=xA_Kc%~;)vw3tu9@m|q-;F+&kGIJG(E#;}h{hm$lvC31keDl7_q_r(UI@P?8 ziHsq254{CFJ?uAy?Z=XR5I5+m#0xdGh&(>MCdY5@e@iYsg2Irzy|p+u5+Dsc^(zKa z4+7O=oMfeo17(fWmDS@9PYT2wF2y8yQBM)R8pS=l?X&%vE8X4`jg5^M?1B#qDG?{H z(ktOH@Rf1GjeboYwZN&bTm8cGiI?UZbhf+#{{h_mWv4lo8J!C*;S`#&^Qg`T4#6^Z zN5ZdR#~6N2+JaP4<$sPTCPoK^F0V^_^}ag>=R9bN&JP|+whVnu5KdltSA_Wv-A)tW zJdDb0vY;lI2IKZ$PFZ7)xs|n~^T|ox18`O7y*6c-t}3!c+mNp3EXF2T{WEn5WlfHl zI&VJ;QiT{tP!!5(XMZ)TcU0)!>(orwXU8VV+U4^sAz!%`QF{Te!V%dj!x}^b>xQHT zehd1GE<%w!7?(UQHqR$h#9}NUfDTx|z<y@_9vvrTQ?%tCO{hyGUExv+49@gVGUFVS z+0h}Y4<4ccQnWy)KuNmXY1P01(``g1JZDrZbkZlX5Ev}raGBmx_!R(FYP3*R)wsrX z@t(J|)S9$jfD5n;+2c!&0{$*iH{g6`>b4FanJqHM6e#&=e4E3n<&F-{(zLiitW`A_ zXSxlDPa{OA!9ogF+EcdFgkOWineTt{-)YFv(NJ$Cek8`7lbjFY_<_EVK*<n|w$`Bj zHd@pBlU}wVrWGUMs59q3C(SQW9H?M&KRIU6mt|!*gDXU9l{uHM?F1*d<T*Z3?h5ow z3c<Y#YW;$w!h`;7vmd7aSnoC^kbQ{O5dO2uwhKTf)30~RgaxE4{P53ovd$2Cn;?z> zwfE74>C7w1<WRAIlrimU(l$(CVB7`qDFSD&!(b$Fg&KWymx8$r*@)oO-1K}kn`DR} z*%WpqCoMUF_VdfFB48Ep)#daYJ&CClz?DR<nI$f!nLd-yx&dp_YkLnXf&Yy!f}H%x zNnE?YnDDkO4pkuU5#7J?>E$VE>#>rpZ-V@)gdssd4!azPLQ${x!;|40Q)Ggt3)Ot@ z04Q6@^2<g{&z^_!SDE<COoCql({Bb^)z7@1tSa;sa9`7bJgHlc)fcoL3#huft_AcV z4a(jHa{1JKmwROU2i9A`x6Q9P*Gi;uwkm^rSe^aKB2dAsPq_=mEh)<3y`RJH8Co`C zyHzx=WQo{<Y=wUXCoUF?`!T!s{fn04jHljgaaIxnRccI|yN7CG(_f8DigvQ>J3sIh z9dHj<vUw?v#6bu#0`E2W@!Bl}4~Gh+QT6I^;&ib}j(#j0QzGvo=*l?~w{gV|5T(_^ z?{t*BZS8RWQilDYl)<>*)EQ18N~8ZqE<X8kJNvBs)vnSCfuq2)wfb^0THroZUyE;t z$U)uQimPxKK(L$Ug7%B9`jyzB8?*KPMbCYYfQV)KcfWjkKQP5VP1&&4={rZRKBxA_ zG3fB3)QiwW-f>T_2%n@$t!*%rcV1!yk$zd5@VqR_+Vyc>OK3vO*i!ujQ0;K0QQOiQ z<2T?r7;iDC9v|Yza<<er-Vsx~qP4LC(amBd(UCfW?tj9{6-p{CWt*})ha(j4oLu#y z`?2?p!gY`5Cl({CPF|Z+dmuz*cpo_*B@JVkHlru?EHq;MgSy`a$u8A>Wl%jD|0A}s zIORuC!k=PNt9-0*DdIk`Rz)3j35khm)jsOI=>cW=(QWYlEz}`EDqmM;bVJFeSe^#_ z-f!%&6zbF*&>|b3-wT%(_`eNXNKDE8=YyHL;>FtxH=S*L=K3ZhD&<hJv1scmlg97G zr_rkU%ggg#SxsN#Ipa6y4*}Z?zxG8p&i4c?aqaPeWRLynX?+=LUp2fu1jl+qx5(c$ zv^<=#tcnm)ufNhUdsV-ZJt<v{)5Zb}*WP=Je!rpBeaAs~Cyg2qf8Il_-1iyAVk7RU zHBC+_@3VZZIk>PT)QmskE#w=ez`Ev{gokE3KM%_~_F(husI<U3f<FGVJB{^$rvcu0 zPGD-+FA5wh2<)HE78qjrS&K<*xb0mQz2KmqC)au(AX+W~)z{!2t);6ienW4X-Tm(I zSKnk7Q<K%+=>T#^8nR8rRLc{k2b7P7yRo>HOzBFSsfQJ^@s`%+HmJxdeI$;18@W1n z-(+;V3>|GnHcnd=zs@;r9eFS*y8Vb%)ZHHKvBIs9?-vZS_LGQ^BBQY<n^2qL3FMY~ z(K1{}G%rB^Va1unoW$TWo9f59>=fBiVzY&?WNhm~fqMxIHxX}>3<)ZQkqj4&W5LKI zx=i~r0_OP)lI7TLUUAqULOC(<4lb=(6m5r?O`CVolr6FlxhkQLhk649ct$wqI$5G) z85uph*HA$I^@x%$k?E9t(f{JWMrgLaOgXd6n?X>qs)@6k-8a_9y<qqML;ZqywP2tp zn+tQ%Hw;3$BJp9Wvo3-0m$2*B&0`%N5b57FhyMUFZCGo@D~_w8sPumPLW5PkrH`@F zxbnWOG;Tc2qs=dOBXHc_H`UTxF7wX((t4<3hA}6fdsshKmG3r)0v8Ei8RB|=9a~Lr z-tZ&bnt1c^$X^4a>WJf{INnH9@s;~U3-e2i+v<D~{8GZV|GNLqX0GaeZm_&d%mIge zy_~S@b<v$*k;jcAcsb<#oET9zpSAH_3$G%%t28^|%);VgMbWmJisdW&X}8Aq6d%D) zqTaoQB+WlhF*Mu2AZZqFVeLGV7>et|W^jnr5HYwnyhS9MaK|Wku~5jxF}+^qNF^bm zO)!Sf)5M}a>-P|6tB9RwH}1M##ut4Dg<C<;u-QzxuHLGy32mu}=Z`7Cw)>M!TF^g* zpU1d}Q0kv<73zB4#I6wgCi9*+OTj}V39<B&C75q_N&yIA9{cG94RCCn{pT&$-rfA? zXKB34{iTs&@4fnY>YbWOn{GF~qlIV7W=&l+ziHFiVvuw*y7GHwT8Ce->^+ix&&6+k zHmWu@d|XlmC6O!Wzb}xFZhIoA>tM5X%^82FnY=CAK<}>*wCz?hwmYp6()0DNga6)= zkC^#yXDffRIk}tT%MbqD_o=s>@#fHc{-JxU_I?P^q3IS`nYD$)d2BdcKjyudJ7t%{ zoERwAAHXA9;`;2{GWb8#n|13yfYQ*@Q!z4>C2zJGZLXS)-WZA}A3k)eZIAf#27=pr z<}2aGLQF-v0UD`)X1UOP<6KF3E3@Bn$SSIQ);(OlMaovTiiV0U;|4wXE^x(pRvK7! ztV&u^QR|TTv)XW)vh`+q6RGGtp6(iw>qfszcnYS=DS4Pre2ckl5ftDoLlWQm^<SX< z9rZ7%#3_Gfy_U;Y*|DPgrs1gO6(^~oY2u)l123}|3NLVkot^!x@~Fmwr#dl$fzL}> zl$rkOQP`B$J;9VpE0_1E2m;c7gx>K~PU${9GLxt^nk>6PXPJFXT1+>4KjwzI5?_L% z#%KTmrfL$0d*GnhFFyKj9-4^mLL4u$(o{1`vLdXdvG}2C-S|I+*za82w>4*3IIo8a zO?xm>Tufmyg$|NGIRMV9UB9NsRHSq4H)6Lc3q&uk^4^Ya6Uc2`3sjOsptFEd7F+fn z<{2$%86<9%K?_B(+;dh_s#Zfx%<n*}4hPJh9j9$O+VRnW`<iL^L^aD7rF+nY%<rh| z)=ZbR%IDORr55rjLDY?olr{QItKgH<LP<tih|{TrDgYFt>lxYQv{=oH_x^*UOS-s3 z&D%`1uV!yNZ%SpC<6K~!0wqaF&czE!LngdmONAVrnb)&|oC%8;@>7GjR<>3AUJP=I zE7Q>}<l{(l-f4`-=9uJ~@cTBzD-uWvqZpqSfs)rSZ0ih>lVen{PW-UX{1s&!(a?28 zAZ-=a=KXwst44HYAk;gL)uxY&bLkI(_y%xe-cBR?Xm;Cl2O)YMD^p3j=)TEMst=Qx zRMeP1F(!49Wk5$~g8ky-3EMxQ7`EFCKk$R}k=SxdG|B-tI7APzzrtCOV^AI<n19#2 zT{YVq#a$y~uf65;<e9>>Iwk}Y6ZQHiszD(@-ONYxg|((9b|$LD?m;a@y$L@AJ9sH; zMx<C$+kFG+N+2z4S={L|RWa25vIS9PwWkXu|M2(ZLmPp#J$Te}gsD)bq4gyn4lw$1 z!X(Cocd+O0q?4?hyX`TQ2^{n89Si?k3}a(E1bU-lfafHpbFgMR`^E1}SjYUYd3HRa z?WCs<E%!<>twR&R`$(SL8kTgQXSu%`wB}Dja~%^Jp(Ns_Q?%(x5l%wGO<P5me6Wn# zJKP<j>&li5qC-`hg#rPKp>FH;#9;_3KsBkerL%KpH*z|~)v28hzs8k>FzkfR4tzui ztR1Zwn4HUmg%$o7;wlKt*P&yN4R{qiM^|as_=&>G%-KV1not4FNIs(9hgob;7^eQ+ z%cX80?@I}Z<977PhN`@6-Pa2K7n!l1yG3Y$m;pcp-CCH-?i(Ql$riWGHO*_W8<Rb< zk{A4Gp$smAn=81ju~jRNkn6IJis(Pi9Nj+utn(TvNb+4;6*4p)NJQqCzUh!iFn9tH zJparBKK&7=6f-1ndbW!FT`=0*{i3X(I)~oXAIMs#l-k2gc>#X*KDioJs1E^N>Vv|T z`Kz#{qme(gs_qPj<pmRgY+kMK=U7q=LYJ^J<T^QprCZ^j#GSHOn*)bXU18~eJ2peU zZ27$N<MPd=BsZWC9`o8}Xf#of&0YOK75$C6WmwdL3<}uA+$oD;uNbkbz1%xa<o_#1 zwY>B~Yhtr@f32HxlW)$t%*6ebK~uIk8Zzl&=+Vh~j4+JN4k}DHDZ2cko59ws+2>EN zH1Qkh)1JjcBN-q|ac_JnLq#FeU3Tc*n86rNv3$cjMD>B|_D|vZxhI={V5~_>i8^q< z$<Tq#o_y0XC1SSovS%PR!VKF};OX!dg|!xbvhW8}oDkou98s7Z@g*AMiHXy7I(mEd z0c`4;!0e4SwLxZ!uO#CS^+RUH|6ElyZI}~?vP7pvw13|pU}ym@6b6gftc(~*-tj6w z(JpD5P7XmzKcQqDpxgLDii{k>nQ=x|o%{5mT|}_R9zg`!Gq_`iZabnl%CJkJ+GQS8 zf<re|>ATuPwO8!_^V+YbSOI%5I8lpfjo`y<XwUv^5?RyH)dYGi_X{n$h0Lz27N%Hu zMwqg$iP@*MBu#~5fh)ms1+0g18Zf75V41Y-@b|E&=RQRyB*QFfLwehM6Z<hV=lg$v zH@6Sjd#btui3@omyM?0Ge8hBI%r<IgHfr*7x(^1Y{U7{I505^We;H!dBF8W@vur7W z@^)9#7M`h-sZap0#j$K&f7$|&GZr4GQDHDzy&%YvyCRwC4FuqJ3OwLeJJ@XxHMbr> zrV0%eXcOjXEUlS-syuHvsb^H`cT#F_!)w5k<wVJOeX?|idYWk}iF2J6k|C-7Zcf{1 z?jI5Nb5iMyvMb*SzfPr>Hnb~A{3b4PL||7Mj7$eUq&NOZV}`7iDg5QFEJ6U9jsJj> zq#Cgdh??={B5nHDU4fZqnsL4HA@qoJ%VeYzgZCBjkzDF`dgE=lzY_Lh=Z_luK5BS7 zKhcosTHJU;m<`sTqs=r!2KBp!aJNf+q+WUa#c#@RI_RFBh2w)6>GLbwq06fDHw~8i z5Bb5A$}shgLdfqV*Q*x3(Gud1Q)^;-OO4=qdn&#oah&b#_@`1<S=o4}OiOT<U7u?h z-R^J+v~EogR-nJ#sx}?gYe?IEHdvO(Y)@Jp!J$OKbT}vq)_pi*OxaTW@c#gGL5sdz zTkVo7mE5h|AAfP)zAL?iM}D&n!6zFS1HnB;YrgQ0j5RL`OtD9q?A?KQTbvH%eGPs0 z2<M+yUstUwM)!;D<ouuD{ZGq0MBtjaiiSS`?Oq@GGTkfpT|VLdGtqAJYr`1F&e<3l z;<*L4zL6D`xjF5M`d3h055}=u-B?PEHyfjF9|zmosp@_MOJNkYi?}HPln!~X&@mi2 zmg8&vVzrZ++S*^e{Lh@q{6Ll(g*+ZQ@vN?$^w{~jPwg0+4*fk8bgAt0Ic{ZpmE6II z5@Um(!oAa6@F_OpXk+Kvw)`960c~SC-^LWkz^;3NTsUqF%W&16A--sSH$6OeiCBNA zdwO!buWKGhW8irwwP{m-2b`YO>Y65>rFdgUI*z0~wS;9sKQp(W^(MLvTU_wpgKf&) z%!XFsVFYd@4*s8wcviLI=r!Aqv|dc16#nWl$|Cw;4ozNboaOmFDLN3e<gAxgy!$Uc zy$>TUdAAmR?y7X~so$Gs*J%F$p3Cqb;v2K#i~j(&>QU|hZHhdQsGmSZa@T$yww7?| zB#{LHc1A|u$b(k@0Jk*IKz1$ZwOIan6%%Wka-4$fPkn@s>0gKD7~0s%^2Aqnlm4yf zkK3>~Iyg)`FxYtV!Yaubx85JOTNwljR!Ehfe(bYgWaRhFasC^S#eZ)p;3JklfHm0O zYnJbCF;#qoen`N{2d6w&nrZr#tYMhB10)^2c@@h>Y7<g@8MY#msY+DWRM*~#u8#Rj z4oM%+nyVTz;Phco$*9_IBrxFP>IF!erLh_6J${v(o!!k;Y^;l_>AO3I{{T!=t^iaU zPJKtMWJ{`BF*ziT{L~{?g)nxKdVozK@|rg;hADajo}SfqHGPDI1D-N^dRH%~YS6X0 zWK3fi$MwxyO<(vzgVXs_3mpc+TuGH2bf}qMf)Df+lA5&6+ZH}C=}~`bT&nP$dl5`l zI%HDZj1z&!O1M=Q0Cmn!;aqZRcSn$<Vyulu?5<n6=bn{GzQQ{lpe{)F$E`K}K)|kW z-(4IM;Cp77_N#(&WFMV7v1oLscPAs$pK7Ua!B=*A^{g{<C-F<j9CxZy&mmj~{4i-< zk3(8k3(nv_8m}X%1O^!T;<@S4;z5-xe*;Vw*9^+|kd^i{a<IBLbjjlv(x_$QzH6lo z6TV9Uf_itaJ=1L7*6vwu3y(LPV!CMl#0mZW4>;YyKHSr@S_X#rx`_|p=x{xG{&=hK z>NdpZen8;k{Nl3hZ7n2C%_6pW7+_E1T9DZ3iZa9w260U$ODzpf)z|<|LE|E<N2ox% zTd$}hsU@bJ7I54iLCsrHr33}=bL-gEqCP`q)}Z-{uTC@2{#9NrT@`*zHzb4HpH6CK zO+S9m;>XvmLh@K!ouduAspItZr++b9$dR>@PD&l$rBiKbnoJ1rD#Kpc9ibDaKq{1) zt)ygPJoC>S{{SkVnC$xrCao(Ux$>@f1DdYamooB*MtTA%^6HQex}9nvtB`-ya5>;} zQdOT~vs`LJ3}YwU4k?n{>P!d;$f@5}ox#LMIqVH*&8ptAlCftUa6c+zu~p>M@<L?Q zR&7sq7~?(aL#SU%oJWtyRbRDgPs2xnNDFIks<0otz!hOEZ{Go>!Ke`GhXj1I@gAzr zjoedAm+D#?b;&qss`_aTW5dtXcB(&P)E}QP4r<&wZQHgQZNAliB3tt`qtc>93Fe*v zJ#mb6sI^@#&+KwDPDv_9-s75)bg4s-)8JrzYQCwZqgg)riNHPo0Hq%yRlAk!Z1m5t zAoBw<0o8^%JaqK!S0RH=+Elk0&NmbFtZO|w*X%JfN<(zufLLepJl6a+7CQlwPDuXu zG`f`8DjhK!X&F<-NIhyo+G^)&0zQMSK=%4_5rSJh@K0Z*PJ>K1-Pf)TYKZv=Zfq3% zv2N#+f&Mfmp<<xzi#(juWs^!lf&ld6y+a+=nyi0xdSj3}(BGKS@-6=WY1;w2$@zcJ zwNL%26Ah~f=cQKreUJcoeGWTflxmjk#8GFT<@iy5F<Z!%JyC-)c^&-?NZO?2VPNU) zRll`UClKfGr^l;I9I1q39)y!j7ohaiFPVv0PESrMPqpf#f+OfVfBLD(sNG}bw>dte zrAhX!M1h!!=caK$G0Qfr+?c@6r7$k8%v6jM&~y~xHmJt|I@8c=4U>%ebKBCLgn$3h z{FJ<M>nyr_9(1ud3K_6L?sN33I%U+C6UiF}{m@+Q=np+Awx2sQ%X22cs&dWuc7fKX z()9s%s9nbshJ{L%KDia!QmdS<&J?VyjoAqpC9$4>`qCyFETnVP`_pHMB_Ng~9f9Jb ziwneGGWS093F;(Sln|s2bHP0;KIIkhA`I<6Fwa`Gxcth@8zkT!II64zlEiX9gc^6Y zpsY60V*oIWM$Nk*j!if*Tq^bHN{(_l9B?o?8YjE8fbMl6!5_+nzNM?1`WrH8QK3_o zM+Elio@?lz1KMv)k;|Sd=ARDTwEDa%0ocrNeRw~meP!Ty$DbTvdRNSGmMhudB$4;L z1%&6vsYv>Jz*;K#u}DWwYw2w|0FaI=$ovy%78GoHSH5X>o?wkVNU!sU#SF>M7?hd) zeJ)%!3J_kW=11+o(_BpztViehLW9uazcg)9?knX>xQeO*SQYMiSLpBUpL#BQT$XVV z1Z|JYuk){m?k!c2OyE1m^RGPd+v#8Ehk^VurNB5mOg-81b9q1bHb0QOSj`vu%{o$7 zR*Lk~{LW(G3x;1@e>(a<z%VoEHi1XVw*LUuzI~fwK_vLc91q66jQDIx@cpZv_TT$4 zudIZxBjPGWMv`4w-CJ<u71!E*-gBSQxhqawk~$jetq;mKdRDB#D<n@j`Au-LHfNE& z`qy)E;ZXYGxK}w6AAjpW7*fiqe2(CdipP$9Yw14^uH6eyQ`$41!xi&sP}gmr`R3>J zBEFW<%Krdm+AcuAagWBka4z*9q05*nQeLM=5eJh}`kuY&vP&9_790;+66zzLop|@_ zUZLY^T_f8V6p<;|Nge+HN{n1U4h)KE{{U;X3Cn?+E{d}xGV&CC<rzMd&b|2=HBjE$ zZ)V-c0MkFTZ4i^YIilp**uKq`4UW}O*fvP?HAS_1MO*{Gs_|;p-dmg=jZ~T;#j-{J z09g)yxN0W<0FMXyn#(%PxOe~ulr>rPxQ3kU7mS)sqWKw)Mny(OYn?tIPRUUnc%Qo+ z8WGCQ%ErQ5>BLLIj#;tLSA7f-+eEO!Fsd*=#QWC^;jLa*vXQo6nGbJ3D&Dc;S@ipT z$eX2pBV2d&KEkHu^1EDvUn+GH0nQOY_BB@00He=w{^<U-$iaQ7-ppimVl%mk9FzH0 zq}Mu=DgdkodUU5?I@}fONO|?ZC-bgd=G6)4-0?(FYK%#I;~@3!GfK=f?rvY7l>zI! znuiOT=QRj*HWBmD!K$rqsXy`Ji!r9Vos^eRBa;Rr8OR&6kH)IK(q}B=C)`&l{hg?{ zStuGc`*n8xXh?FM`kfnHL;dyq>LYn<Ka5r?_<r@fx%?_o;d{2~B>cbn)a=Q+G!CO` zI5G;@(zQEa;D^r*#dBlAlm7q`y=qu!V%dV6?!n_djTT{ab>~#KbzHIO-jn-U*@FcK zJv!Dx*+zo|4hA_jVJ+i9!Y?DKKmB@KvJ0ZaYWCp=41G_(wN^URK>VYTQ8W=?0uTZD z3W!5)rvZ?A0sjEjrOLsySpM5a{tkkky7Mw@2srlV^XpXeZiM{f=mjCWvqi$N$m&4n z`O~utuEvyl#qX9C0RZ*@dexDtUuOlR$@KpK^;Rvmy=*^n5FMGxBP9N{fBSU?0-#@_ z=Rcll@{60ulJ8N}1Y%>owh(Y@xYDj-k~EmY3o?QUZZqh4^{+pDY&w)9gXjnI#Xc_) zU0cQ?iQQCm1OEWkTE<e2#YU@J9=&a-+*?Sq#G)AxZXRC;kH@ulQG87C1*BozeWU<5 zYyy9+e9C-BddDT4pQ+6->)MI?jVSNcy8d<V<@`&J!Nb|olI85uYw<i>-y_TOMgHGL zGkx0E?>?tL7Bx9g=E)Ed!!aah+*UQ;#Geyhz#z3TTJ9M00KxnL>0dEj>e`%%<_|Ek z`sbxaxA)T^V2pme*L_Yk%;gstxUWdB_$SWc{5j$3GE<HnHnr8a_%~zIHQhGu{uaEr zy3NdO<}8x8EKg&T-nrDalI%w(+Lj=5xHAqtdj6HndAfm7g~>g8Qti}F&6XANm8w*$ zH&Ux@zOQrbp%}s{5YZ#90@gKUNs+V0OLnT!PXca6lwjm>pYX0-t?%6JVt>!!Rn3N@ zoNw#WuyHg}I@`&t76%__EEx#k9^<A+t|A*)0$^tzppWsVojU&jC1iT5{h4zFfxN<F zZ_<~uE1aBJ!doRruimMW7*ON)e?yvZ(io^FQ-jl*7fDQ?-k;1>C?Mt(q6`n+40;UG zZ?%tZPbblT`qd!Pp&T^0&j%;`Dpb?0Vr`~usOe9Y3hbdPT1dY$gPz!?rNx?^j7V|( zKj3R$?3!;vz>iD{WVSk6uc;q&jtDezp>r~#+(4v_$MB}S_PFFR$Gu9Ar^5bJ>NET; z*#7|a>d)FV<oQ)c9eDo$>(jCevn%aZ<2z$5?@kv{9uhqN0H3XBo68Z<Eg`?O0|GiM zg6z+GsgKIB5IuRQKHq&4ZIU2Y`@w&o6|CB%3IPk(wkjsoqsRKJ{RJ8eQHgVO_$q@3 zz6L5l+JkZzp4C*}OoY9%W7SVX{uKCiiKJq_TPuHbu4(8NWulswsU7h_gHt0U{W+-G z{_vb^Z%p(R6J6>+{HS>KKjA=?%PnJfFbeEHl|*#A_25h-U+$m0K7{qCe$lD8!+gJu zGdh9fxL$o|uzbsrX-h6up9hZRSbnu~4JGa0a$e4d{r$u8Ijmz9%)Ch;<DmzUQ%4Qr z@bhm|?ekLQU4rkq)I|iqVeZyIItEZbP-{*rn{^nJE0NqbKb>;PZ@hCmeK2^d0?i28 z&gbyOH55x{PH*%?`|7~-J@OB@KhmZb-X4$uC5-xS9P#z8N9<4x?ksWdichjwH~P+g ztSE6obG0++Cp$_Kd#`%0B-S|2!Ph6JHOT#kPeYXeA7;V*DN0*M%Fe?*L1IVhDjI#t z(B6GU$2i-_K9tYxOH5!O$UVhlwdJ&2%e9yLz<)eo(ypzi51Y!|y|%DF&Z5k{4MVBJ z^GC3PaB+&ypH*GnTEud2bB;Nvj=iMEkCl-7sXwUAMB4tJBNbAdd*J^7DwlGXp$lJ6 zleR8BG2W`ap?ppjFVd_XW)fMpf;~W}A68&KSdZoFLK_t|9Z#T);BnfmM|Y|cFbD+n z=~PaubWS6O^r=Pepx-czj<m@&w(5>E(~nb5NVO+)-rGiclUecIYNY(DjQiEtqSdDZ za}aatf0a@t-?^yvm!?6wT;~mu*N^_SMR#Fx*3&D^fXYDNW7pU9s<K&YJJwQpTxXNp z`PF-?OV@$YNjA2^eR)6ORQ(B0%+j#ZNYh|RCpc2r%8c{K=e276g4H1h&LM&4oEpxx z((a|xV1G0!<7pV=ZSTp(KRVI=%mD`Zeo@~z>r3Pf`I5DUog$6XV~)9?S!vPZbA~>- zBvQv^0Qp*MfxzT{T6ngy02Y%z`>CQzvd5;V`LIC#RI2(|<7q#KTAEvZJ7+EpK^%H` zY)Es-1Fu0xP~{0V4LTM;@Rp45NL<w8QPWqLV(rC0&C<XiF9+I_?K)zNh`{`*-^?xK zH0tpj0SE`CdwznXO-*;<Va^6XrlzH20oxGI=}2`*v+}|^@OkU`b4C2cZz9}!tfaS> z4<vQL#aJ4J=^KPO9gR^-{Bk1vg+4t-?M}fE2zdh-CYV^#it|y%OnKz`RE4!yBLml- z-=`GVukOgehi~Oi7h03#B5gkPb|Iht()@+EdE>XZ(_#veOO|IpaTN6@)|;WD!+kXV zdfge36i9<=;dtpp4sV|R)<wcP0vUnaN#hvwt2#^(+Ql5L2##PDJTGuPyJojXBRUxj zs@q$G+Lk@YrJR7Bk7~`ixoMhK5bp^d<vY18>58f~xe_&3wsl0nZ2j((2_tewBn_vN zj%tDzcVIy{IN;J*YZjLQgOH#M;Pn)e+^iBvBgg=Q^(KZ17F+E0sU*zU82OK0p4Els z+T0a8_xagq$v&Q=tu65V^mmOTh`{;RJQ4KwtZz2SE>uRRAeZ$SJvpa!swxq8x#+$e zFCL?62<a-RIN;{K(eN#}`$R;J!oD@|^AmLgI3FviBy=2#`&+=EnIb;IzdYl2QQ+i$ zwc+d|^`(8!w0sv3Yj}Y6uY1$t5XI)6HX(1wSBZEIGGL6gdu`mJ(?PtsCjlUCe+vHq zHT*=Z>gLj;9_b&_nZ%rxI!mpW-hMQG)i+}E#}Wo?W=H)oUnt!W(?@9w6C|W6`d7Go zeby5H0LC)R#~_zIeOkP)!<KR0%J)vZ4ix%hy??MA2TnOx0%7TGB~IUg{AtFt9%sf? zqaTNx@JBbU>CbC#x1xc_KEB?y?|%(>@b#kq0Mozz%xlOzZ+5E)vO}GqvwdsXeiuW( z!<LQHZLt3U(2ac70;HuyPNBh@(We|!rAK6T_S`n#dh4x5R~_q=wMO5T;|99Rf4Xs9 z*F32`NbimmX1IwC7D8*Wy>H!;)y2-)Nr)U!^h1KxBEORw&PsEeb!O*3GaB}-HrD>) z)(I5+zr5@0)#7N_j%OVJiGNkidxo8>OC6jG8u9zgasJhIU@gq<a^`LFq`R6!OTQpx zy4AR}OR_)KeQGhO-Z0@vz~=(2E}^J53a9Ij{<V6V9#6bW7Kc9P%!mB`G&YF)veFu2 z-fDZ;1!J6Ipb~11PC?E+fBN)%g6`XzKiFxKS@`CXO(GQlmi=osRn-X!LCbaKmTO&2 zm2=ND{zN}g!O|?0W!t_w(_*l&iWe-y7;gU6Q0dqGS-Q4rA*J3lPnYYGew9l7i208D z9-1B;mEjM1V?m}3Il<eEpXXIqN4PRVOrOAi&ssyGIn!J;hrjcssakhAA~dDxxALU( z^tI`a%Bqb4K>4IJ;i14fx%}$&GSKZbO;<{@(<Yuk0wjk$IXD~-dggAm%WX#M%qx~- zg<i@%D!2A!xPV)<pSuiSZhwf3X0@zzO*$YRLN_<RK}y5A*&@U`g`6?%RL(p4nq>OS z0~kTn)`!^iv^dDBvqhzVKJRn>c&YVee2Q(ZCzt?El=(I9EeFZdJq1dVX@SEP=eHfG zd#yTI0;=@D{(s7i2JhI4%TSi;LnM(6x!{$^t!BBDkCp+0=%CiEy~T~RNQ!_GGINe8 zPNQr|8wNY`jy|=rim{Vzvoq~ii@=8i*KIsp-wf?mJmh3!^sC2Iw!l?P;Cl3@4Pr34 z2mvD*+s#&j)X(#;<#4+W4<z(Ih7Bv(-pW4n=hK?A{jlghRL6dl)z;)};Z8?94z%ur zQn9eV+!Q(baw|P_>-ldOhQLu;@@liXmI^`k8T{)W+VfDA=5#~5JOk}hr(x^dzx|bV zbNj$CO4?MUWXqH7Q5p3{!P?yW(+rxEADMC2trzkSnDe5Na?&@`(wF-YRQ%E()BGyG zmwrbq2h`JAE44Thf%w$7<QjJ_{{Up#XXby_nf;e!s=zwnj(`1iK-%0lCSW-Aru~La zH&2sINv(p*sIXaSv0X4M4)1){qC=-4Jb*eH=U+^3J!SJxy=Z-n2rIqmYKNh({{Rxz z;{cxARA%Q-P&gjk^{mgaTjK<=J<f4WHdd?DbpHToe~lc%w~?lG4LA-|cg`vUt6C`e zRAQ-l7K4l^{{Vqge7jM>3H<*6r5EnUZzA9J+y^PJbH)u(k6c)0F@)gzRGG5}1g3x8 zH5BsMAm5cFdJKPBSMwTPMYF3jpO!z$qyE&n&J_>kRpQjGU4B>3wgzcVp$O!Y`hSHN zL$eZ1Lin7jo_(+?H@(!48+!JpKBocTZvOy3g+etoCjl7x5=|?iq{$k6Py44Hgws#k z<WLB}JQ_#7$sk5gZsdPDXWJ&m5+BS|+;(G9Jw_Mu-Li!6dy2Ac?l)x0J#m_0y19`E z5o102e@cc6twmqSbnDQXYtTMrqir^KG~Yl4I9V|mX`2Vo8fhAf430g)r28(W;QZq! z)~n<z`HnVAz%Iw|G}C1o{{Rya=uiIuUX$!PkYHW7{U~iZ<l{0BG1zCC1)#5MZ0baV za{G}@RJFHUNrnfxImhElbjfftDdv;=EOcoyJu^n3p=c$b1Lcp`y)NrOc-i<?clKmf z=XaP&dT!}fqqVWxFyM^mtrs1M%+|nTE0175#-aO7xIcT3prtTr*v{eYifI~fBfsHA z{KDQt9{vDvl7655s)euZ$>>=2_NmOecn5)=)X%k9{{VDmkC<D?sM@2(et6lJ`@=u2 zNHr4tq*)(AY3-?6Dd7S9YO&Pq)R&0>J;p!!)R^@w8>@r&VaMT3Gv9Pr$K)v^*46$& z9vje)<xg!!Z{due_zfBhP+4^&(Q)|VtRd8x@<`9Qs$bd{+2S@Ig(}_ZOg`>DxT^UH z-?@FP^$AYx`$+CO(xs-PpX($HJwWEEiFH=#kPp2`6I5}@`f_Nukv}s<Y-EsR&5xx# zX_-Gf$B%l)YYjz??=x#2^b0*fP9>A^12nblA2WPdz!zzhpL~i#Wo5=>2l1?tVL2Hy z9(_2a8ha`I<j0?SL1bx|G}Pd=S7>yOSa8F-@&5qps^4OSpWaA6@2YRuEOY*|_01O@ ziQ3}D2`-_R)Bq3VOl#dFjH;uK;DMTe+iB?BwrUoSNgp!~M}G8NcJd}ST3$d9h5rC_ zk^HJfyte1cc3);2hw`do>raQDmB-)>Oq#Zr*+BepOF-0^U)?4|QO6+v0Ca=;Q<qr2 z2Onc@Gsqd~_|h)5r;oS+KacoStFFQZ79!t7CY_L7_9Ty3zG2J~C%H<|ivIpV@(?rI zf&A++b;*u)TDA{Qll*E?s@}NyEhIni5GisCnVZQbtso(eGth!R$*NkNrlBhD&Kn1# z5I_3$koS7KAKk#op8J5P<US&hZO8zQjM@H_a*vzL>+I}x1k$42DCBM+bKKRmvX^EV z{{RlZ*0}iYwYcH?G{rywa(8i$LOp8yQ{FKKOk*FybNGzZwd5D`IuGo^Tm5kuJG1HS z)~v3dZa?L=bJym@VyRZf`4}GHuk{seZEa+5!s&jQ0MkM9FTI_D{IW_<Zd)}BQE7+< z*9<a!O)^UxRgW(*C$J5i`&4jR*ahXW&Tw;|#(?|F>NLPGIs7`*o^F<(D=5#<3aV30 zjQre@>7KOpZ8Qzcoljg-OrBpNSnl-YCv1Fms|%>wgO!V?fAT80xzkm?RmN&$yRapQ zU)GEHirz)F)ZoYh4&8A`Z`(W81Cn_8ds9ZCYQO5!anGqWH};rg{-X!C%4(DI6}*d4 zUtW=s7Ckruq!)MXovBepsZKx%3H-5ER_^%Z2*~`Z`V!Iq)BJLeTl+o2#ubs<Yd#Zu z0Dhe-v6(I8w}tcOox!}t;GFu_K9K_3td}VWGrHi)-^EwSmtjW1X$Q`8j%#$zHfY+I z?n6l!QO_eB;8jc1M{R{iu^d!ZqF8@;&b*8%Jq9Xov)nK&><K*NdQ;sB#8#SZvywmn z_v5WwdpJDTakM@#HyW!HU%Vtp>Q4ZV@Tl&fWp&>pxn8FOB+=5t+*^Yg8yQ1jU}uq& zQCL`7wDHDN5>Hdk0H?T3*Z{AoYQ3XdlXMJf#24s$QgTsT%<00?bt)($z0kBA-UdY^ z76(6-eX-!3EJh+=zMqwR{{Z1_HZrl2zcI#Z><<OlBdmBB;8)`wAn^^b@`BkP(Od(= z@r6}BtolE}R{J4{W3aBf;!dJ8kB8c&(K!9f575>xhEPPPJuzIz?G>vopYYab%V5lK zexKx5@E_G>nsIduI@g}8e>3%r=6slyC$v@n0Pq!m(ELI8hp0BE;_HacUug-9_wW4c ztkdo7w4En1F5D(qlU#0@XD+MaYrR%Q-w2hTKaUlo<6UX(ZDX>Da`U(Z0mxj}`s0Xx z#{)|)mYlIk{m1e~9j99pnm@HCZ%h9Go`(&pnRO{<nRyIwK9%&}!;52Y;kZ#p-Gt}p z39p|mZGeos50zIYzKie-;AjZmu^vC*M!tHTThvsevPYX!4x@*4=kZCMJ)sWcJ&knG z0C=um)2?yOf1Px(aHAb7(t95}CwFp=qA(lN6~kT4vf5mXWvjT<+z83`>0WVs?BSQT z>KZC8#grrLEL*Ef8vg*}!oG;n=COjp+BO{i_x}Jwub*yXNNv^>{{WuxkL&#_-!yF^ zDJ(6XM#25)>*&?rfmV}9^Ya;9E1d|xuwFv{04kJfmSKKX9Q`U7^ovYkm~rn{Q%uv5 zc_WW{^sdi2D3#k-fE+501y)^dJ=xeYMQEj#o4lN^2&lw*V!s3)JwN)?uKSM5v7IKc zWRvcJk7|kaxrgxg=ml&R(^0TM$F4mpC%MzX$Q|hUko^pQ?OV1u2yxGETCUgnhm!bK z2M4ulzN2IrlrQ7OMJ}mn<gnO3imfs=>~n(eQfVRiTa+~$$*H!O{19?``__ro;13WA zGE%F_tbwOUb`GYeLsiUq(T`SdZ^o1CdZTqXt0!N7;ZM?*9aiq*K^$m#>T%FjXl3sb z*yy^1&*sYi04nzNHM<^}cuseodixK~tF`^=!~}7^1_uM)q!+r0$VF^Yy6ihXthD+( z&DK-?e;TbmjrReN@kr8YZg>Zdywy3sw2Oe<vQIvgwd@}<*wJDP3pxBvT+{SBaQn$E zgT@d409;l8wbdq&Dx7D4e!c$yD(kGY+sL69Y#eqKaZcpP`I;?hZa8Gd20rkp&9$iK zYMgO`!}O*dKIyn=4?XFBu^g|<K7B@c6|LExgUVXR&LihO{{YBRnIKVtvu=9=dHpH{ z(I0jiLxIT8N>iqx0ZGo^L)ZCIyB>nI&ANg&E`Qzs025ICuG|8555Nr5{{XYB$MJL2 z<oBlig2*07G@E_G%duWvLIgs@o(Hx+l|gH&!tsYW87F{o{VH3S^!TDGS$XT;h(EI< zh+_lrslS;?c@F;3h@X)O^vN{O?G?``PI==$<5L^WI4{5DQ8uGvocVl@dK(>|Vg|o+ z%Ei;C6$!k$``%aRKf<gwt7^Z%IX!9?)?w(Vqx}6TT?Lr~U1|rAm>!hhH&S81Bk`qu zWfWj2`T<qWyC+3XJ%t($%&&DGq}XtMJ!;B9e(ExRO0PbvaO!rUKZQOWLiOWT*u(TR zzC+i!Y*=0|-fzmA{he@l^FF4aYpeF;1NgN<UsHSxbw0VLe<6I!pV@MCX;0#5KeE(+ z%Th<CYLAzwu{k)$)}8iC=Ehqeg*%3w%>iL<2hAtfC#^NC><294-=2S+QU1=mKl0ZU z=F=uWE6RFNb{YcPY0yc7j2gTD021NT)AOvE^q-UyAtZa#qOiBxH#p<{s++N-jUz6e zocj8VQ%&WPj1E4PNvv%)e9pMfu1zA^*eCHWX<hd#m3Azf`#?BeDj)58amuLrf000r z$wwzYg*$$n21uoUFt?DSSYF6KDvnK3Sq4J#I&q3V+hG~#G44nB(~hlRxY`$wdZgD@ z4L-wR)-K(Avl$KBihtX66Z|cY%lcJ$b&E{yjZ~lW?Mj;Zk-}j6DLJQfO_^bCbrLeF zN8?W?P?xCN@0x^bQa4CnpF}i=Q@(Ce-a<d!#TpLmklShpqjC7siyPMDzXRH<oBdSe z2pN8)P$$+{aTv$D(ef4i$&Q_L!zM#eof>66VoFAOu6oiDtg!p2KN4x5W7S)eA})Oa zIQ~@rLGvX|Iws`H^#!UyVQ4zaG3lD3`bGZ$-t+!%MFae3O)BM&&v4+5+zMLu4Lg^` zosM$Nkx%<Jn51_<omE{DGIIA4ANU?WrAhsZVn5bObMEK=0Ig2!G-J>9bY~=S_){9| zO!NaD;8cIvT1e_tpK?$2qx(NePO69S%^c6TS#~3BPfZ+QNcGJi*6mY_E04_7q8%*{ z3J2gn!jCq=2GFDH#Vh%Rynt)A-D8hF!AYi_W>z1&h0mrC)JE>sGr1S_;Ly6v0N|ho zi}{7TiGQ_q5<XZT{`mrdsA|u`#(so>NOhiYBhTSZ{?(ZM$dCQ=(w>Cbhnuap8$kNE z_))W2bIDQv0DT&7_LceIMo-`Yr5AdG3<V?Hkx%3wF#86k$9I^jeE>BW)9<o;*~)rg z3{!-fqi{|?8dDac<K|$0GfLh=znJl*+!6C!w?3E@r%GS>X>jNM2UUNw@Aqy=Hje&= zo-FQ%63AEb#VvaW%%A%z$bYPmxBLv#hMS}ThQU6(nrPEqkNWS|6gHg*KRmhr0D)2+ zkl|ycFF+gq15yRj;~03rAAqQZm6+w6egxFZZ>O(T4WDe$aaelXS;R9I7<v*vol2VB zqA*>BPr0a+Y0={g$Iw&7#-0W?XY!|iF>?79{{XfZKP*}NDZ^TjACeNk@B>u-+if2( zj(rK@o&B{4z<4m;xFl6vo?jyKYMO%_tRw^e%|zE)l)Qu^>xzi#voOqX<a;To{{XZv zTkb|d_Y{J{xz!wf?x&`3e@ct&x9&5)Y4rr>@u?bpQO62FKI8l<4Z6AKKd<;vZz12z zo;xi|?-?>ZvJd2X)Z)?Ki6p`92*pzn>MhGY-^9>tF6AG*kx$frjTZ6?`I;+WiZk|t z51?#R=$=pfyq<dQV1K1#4I<!UWZ3lw1XSy#u;y2AJ&zyCj#L-(Hd0G<AheOc=lv?j z+cf0jL}Cf;&15E%0}O{_^lG#;yKn#?{y8-HN#;h>dcKLDm@=PrtI=w@K{$k|$o}vZ zfSP@$LYc)*EG+tL82So)tWBe<k5IBx@~C|B2s!?hBWiIL{{T{W>A^M4!*2{f>bEE2 zK>aIGCzxYn?#E5TH0;T3bT=)v1{|zCf7z>lZ@VDvi}V7pt@SzMIC#;I<ywoZ#<=ri zDi8NaIiVnJAOF<+f4J79vRPnw#DkSX9!3YLKDDK1s3rB1T$TOk8*oNGaqV2RPO@DX z!f(jq2h+W3#`k|~dw4%~e4x%T)rX<!$l|t4<Yu$W8Hh3_=KdfC04Mq4rW2TvP`JVB zG1j@fO>Zm0(h(q2A})SjdV)_{l+7l{?p?@p!w*0?$fDx0(#0}tWkO4@T=9yAc|l}% z$s^?_Jo|7fEBjf|L!FPmkOy2EsdK3oJCLIAc<;>(_5~eIk;H7Ixjc>l{{R~4JS>tM zl}At%;Dgscjd{CW-%D%zrd;j8Vb4CoyYGkod(5jbW@I=9yj=4X=;5U_eIE_s{iZH4 zXVe}XJ4$h1TzCuT5q$-GmEpt;!;EvyeQoggVPemve=_mTVC!M0=zmmjZ4OGYK9kW@ zuD~njKid~yHXaj;Pe#KSS+VX$2lKC{ygwLijCu}Dd<FYmYUfw+2-A0FG4hZ*C>8jh zf%!l7pNJG3m%C4zPvxiTZO1sbLjj4a9n@^SWupC0i@YfSvC)!N#_2(kiTCEb+gyt0 zRMtM-6OS%87(#g9_O9nx)E3uG@*^P`{y~nz{{XBjg;IgyhB-MbGJW%3>JB^EOAk`E zBd*=g<!nQ0bnujRj{g9s`I{Fc!(hPh?m4e&@Hd$jl^wf`FoY64Gm~Cnr0J`xTr<ow z3?Lt3M{mcydsc^IEKo^s#db`YOddu!<X6C8u@L&-vFmI8S{r86Ca@~1+Ub0(?*8+7 z(}w(Orifv9>0Gs^EVa}{oL9Whj`A|KQ-wIMKDkbJ9_%a7bwTowD~Y*ZEmlL*ct4S; zljwBPh4o|aSZs@LQ#dE^&3k^E75w&g(jfs)neF^l;Ila<*!F1yewFX`daFlcZ4^U{ z$a-`4zP0FJt<3Iuc<q&qWU$m&E*lv<n!Yr<-G?$`im4W{akW$-_TsI!r+&zGs~^Mv z0IgnwXyW&*IxQOKCw!-oN}4I(%!G_pKkW@f=jCkl>xy8XQY=UZr*r=R*P~9vyZ1DI zv7^Zh#Qr_0jibT10^L1LQdHDk_cx|G)N5~i(q)L}2lJ}=8OzkrI$e+?mK}X+wAOYJ z<x45f>aQ)8?tcFO=~X7vZm@G9&ImO1Bj!vk^woGqM+kd=^{Ef`c+*@c$mgJ{Yo)&U z3rNQZb4T`L$EV4*R#JBZgN_eT_|k4hu~ELK9&R+z!i@S><*c?4E;jt<mN^`IR!xV8 zpa;*9vyZ!z`qt`cc1jP-y))PU0IyXh%+f;1sMxODD8l6Zb5$Q%fe#2d<NWbc$!Vu3 z>Zcs$phVMALmjz3)ju+qpvTu!cGW>y*BaaTfU59LI#t(wJTb@es?%Lqe5f<cA28Rj z{XbE-l1;@92TU(G6}bAH^qeXX2d5PUzN29TV+se=g<TrefdCSp_t32pv75hQ1-#T8 zAC>s-c&QR=@4dJ6{{ZXMP2vj(^Maqx`cy6ATVa5oG3TNjQ@RaJDMjqbgNz>B)e&iW zTZY??G1jY}61#A*KOKU9S`UaWNWwukzsdzI0VY`%H(AR$&#>e5rX4!zy0d4$82+_I zwS7iG`@3m5?no6~uQit-`CxzBPALxFWY^kD;T5DHlj~CrDm|fD`Nwlu6F#ePFjfc& z_as!J+gO(<WI$JrhKr8Nxw86wra5t+tp?J~Pu>|F^I0QI)Z6b|Z61L;{xt9Gi_CoY zPx%07xLF#89Xt#%4UfdrSZT}Ae@e?bESSN#l>Y$0g*50;k_*V^>gJW$?2R!#p1rba z8FdXfa&UhU#Zdmlus_AmyA%Cr8eW{79f$Cxb{wm*In``-5rxmxR8Z@7h%iEo98w#7 zH8={rJ1s#G)6vMse@a*LD&9r5*X{Rwsv8|Jq|`62yKs@Y`YlQ>tdf`eM$Ci2{OI|G zpJJKTZ!wQ5Odnt=KeTl>8CaY112r@1R+z@XbL&i9VlAO|jE`zAhh{`>wRX?$z{fnd z_)|A)50iu+i8U)~xETXE>GIS=Q@UaDq4wgC?7&5;u;tKauLRX!vR?<-W!s+ksm=ZA z8Q4ZDoyE=cgLG-rzG(Re%(yhmui~A#{3+*3lK`~rf8bcEaLs!nft(-4tr+iE?L41q z1Li#GFrU34{92`tO4G;QBiEKDtNohu_=z8dOtv=+a$UZiC`=uT%ctoIF@?|5H1)O9 zV>wj^{sY(Z6;*V}4<*h&-o;y62zm2l8TBn%E0F9;ji(Q~gFcuQ6th}HeAw6Z6!fvM zPFSzzO-c4$Kei5is=j2~$c8;u(m}$Goy|S9Xq0@4KP=P|C5^LgUY*7TT3ed~7$cM4 zpcH(<-b5eT!2Q(c(*}?2JC)@lZ^3E*0Bo~E!U6A^X}^aD0Dn4K4_?E5-FSK<W9flW z8L!_UagTgekL@+a0$}=MH3MpMbP=EX=lp1}YD5O5Ctt5$@TGf8>3{@)M?=pP@qKv1 zD-(WLr29^ye(JFNMHccE-*OeRz0S#B4cLLjTKf#|_vAmNS8=G$2yyu2)nOK*t+mg~ z*}120AitS->7)Qmgr2{9{{XE_`wS%f?f(FeaZw99*8c#OnD+vc`!am<DWER9l0Kbc ze)9(W^Gde*d}FRa<JS~}O<~qnKaD(TDc#A>y(=)f8H)wrf&Tz}(_URTE-|0Rry5<q zPKKo#Wv^uf{VJ@)?byxxeXL)>xjwxq>#5r}%@Qg8b)>p{2oK6J=qZNM1!7_tKcyP? z7M+SiRL=rG{B+WtM(6k=KljaFK)-dA16GwgZ6B2|xr#5Z+m9@d$Wj?K7wD%y?^f>6 zINAj%z~+k~U7Yz%Q1>59ns?amq!@3|R_;fy>qh9ix8s^tL*{2bx@G9kmnk0HROw|o z`Cd{Fq2jh0SjGroO);f~x`Id2mnsfsP#RG`dCq-uMM|1XK=Yr^ik)S3KOkZ~2&ug9 zoBdfPm4|X6r&|xahwIj@$EVqokT8CO6fCn26+j;5ri{){MIQ7BnJnN;e7~hySYqx4 zKt9p^;q=C8!o@Fk$Iw+G7{`G@%wQ?>9V!-byhwxU1x+MQ58o!BcuC;1W`-6TT|nHV z5lAImWU%~dC$>x+lbWsN?L44hKRSz)i^k$Sa!27&{h};&1pO+ytYrMS{Hky<+nRa` z;;qD4&JKNrSWA$4D;#@NYORjAG~fv9`Wi!MaU}GOl=}3kmP>#zR2)+lKnKh}8n7h5 z&lsyrk}))y+w!UR%}fNu{pA#7Ki<O%PcLcAZI5Hfs<^C6wr=tt%A^sT{{XCdg<eSW zy>bt6(xyojxfJd}GRD1kG5l)MMG^iS{Yk3oK{yycjY<Rd<ME~@kN?&DWw&Lvoh6ZQ zr|%qgu7J;Ow^2%<lOm9W567HW5b|A38zgYMGmhtwJ!xPX+QbBC`y)9~)xS<_RoLc~ zPQzI%EvUQLSjCXQbpyZUSeA0U!|bKkV~xv^mdB-9wz}4*M_;n-P;z%E8P8gV9Wzi8 zJFTj&G8OT_2h*CaR{9F*Xh$8R&wSBBOfe$h@t%MB^`!7y%_2=04I3#!7-I!{@lQ(~ zXxsU502e;lKaYBHXkH+ZOD=YUmIJrf0Mq1sgxc;!BPG0wZR1kJ;kvE|(!FEh?YS3Q z)QzW*I-2v_PYUZ7NB68r9gcE8O7>3y_;%*n;0Gsx-?!(A^E0fzRd5rWkEh{!u~_IN z&!K!jY7WNflV4Q$65WJxFzH?g;hh|^#0lq&*VMiaun=1){HyrOjPZ3L&k07$RDWOa zt{Eh%c0Hd;ltXM!Wl~uC0h;`?_}8rsTgDfY7RV1FA3?=_o9g!~rFd6Ry<?4#K09`= z#-AMP4RfpC&A5`%G7q4yYv`epLbhp`X0lpED5vXo{{TbtzZWxAhQLOW-gEx|(pUTw z!Y;K>?Muc)B*zo7pTN^(dvs;MY?cGBspg&GO(sJW^T;#%*JGY~n%mK=KeTnopfP4B zh<P~R`&adrcb-$LIYmEm2jbXFs=vEZYu>2;0I#_oyQ1mB!${o249mU0&(gY|4c`5- z5<$YGdspd`So-zUc3NbmNCYMa<5YYXJAY-zG=IdupC-P4hT|%@iQ;B-@sy!Q4NnbE zzu=MR_Dp&n#=3Y|o&|E2M<q$?j%%!h3~|=J&7<?H6m<pp3s)6-tt2RlGIwITokt=Z z4|?IQ$yHxKN&K>FCa)u{K3ExpJFL+4$<OAs^oE@FP}ti?CUQK_{R+Nx3%cUiV;?4e z9m`(zXQpqnwvtvL{_{Nh*Q0{cO!9H7-B&u;?<}x{3Q2W~NTOx{55!hvT4X4Rmg(wg zlUQ03ftX+rahmiwna9Z%tjmz=HtysF8SZ`Q=U7dw0)f}osljVvtmR7J)b7>@cK613 z9+a;8i<Nd5n&WH=pk#5<qHSti@DE&YJJKxnXbI=9>qz&OYk&qSzGiMdL{jRPY;vKv zK9x#q`_=~v+yhqR)NK@S2j^6!)ouC(T37QGpJK3|P<bKX^BM0<X1{$sf$mR#dR5hA z45SqK0R!bd4MN(ayF9J`04{U#rZRf^(=@v%q!*Y2AZO)J2l?q!3menM@&{}Rrw*+n zMi?t(1A~s7)UB!A9N}1z%{!w#NgGbMWAjLH=}?_5wuRo@XN>#Rd2cWFu2s3@f@-gu ze&;yAKJ>5TD|wgONK9n0$@ip(NVRz~<PbRI{&kfv+las;CxO)U{{RZRYiD(V^Zx+j zrm7nBCRXlrFzFg+U8sLw>sQv#ON-^$nIG)*tW>hO{_`>R82+^X0PNYr<>rwGqPA;7 zbT-J-)bv147z>Z-OfO)V01;W=U<ZKA$K2=jrvCtE+pa^&gZ}Xpt~8OUCEb`Rs=RxV z$f~kx7Fi%5$DyiEZ3>NsMLc)w`R1C?!a!Cd1KNPMkml8)3|C;l>57H5eaC6xN$xX9 zQp>R_N1z|!NYYtGhYVYc4cMet$jxn0<h%XQBo5$JpY02h>^8PP4z)%rt8X!m(dm+E zbEsN@_g6l-rJ+`2(&`s%Fck5?=BqW;>$V94{sl=UsRJ$*N4FJSbwp#0x%?__hLbGs zy*#M{^{9+@?a=;|pW2RB<pg^HNN;Wk&eqAJK-akF?JrL<K0U=a*-n09?NAGw!^YAx z>S?B1<~wi)rcDdbK4k5txnaHf`n6O3&5dTsai4QeRlRokx(+&3No}sogA7MLlxi9p zZKvA<mSv^;HIU;Y@~BqQ=0yOx{U}=r3Bhyu&=y4I8#NgN^rnZhT!Wuatw!2rMp?7? z(tl>g4&#GHpsYO4WCkz~)YL6`Y@c;heGOP$J|Xvk$FQfZoC<Ih<o3lO{E7>x!b$l- z$5V{ZH3!L69eZQ7Xf36<ZN6FOztWxdXchOO@H+FJC=Zc>Z|6tG#u@3wUtL1zfTl7t z)0|c5ppBPupup);+nC74Iqg8q@3dS=oP57hd7*yYAiyK*n%W?#z!}fsPmgYJ!*8!N z(j~J#D{I%_xX;q9N2lI6Ib)AuS2id+m8!x@h(Rrb^{ZAPx7^QNI$}O|IsB?orUUnx zzdF%r<0B!*9`w_95bya4qF<>=^q3f7!6V$7th!~byZoao-~8sajoP>OzbcAJl1_-< zl~AlTY;6e6(fU-2YY6D0=};`M7F4Oh>7FW3^ZnqX@iYaA{HVaeN|419FCcvlRr791 z=V1M5585%icppG$3nEV~IRSBxYF)qGI3l1cA^Xzj=qU1XG98ToOgHdJ%?<$sanE0B zWOnL*ynh;U+b3{8IsmDkDY2ZbN#@7gineW`#(1i-!ecv^kww6Z&5U$El{C&X#!t{x zsOrnuRG7;2#Q;_FiT?nWWd0PsWXGZ!l#F(z12h31`TA`G@uUJ`e}q)0+*0R`JJ19V z9FNN|&(e^rse`zY?N)MEnmY49h>|szki`8>JIGFq59df@0PbIvsmU)zm#C(M#Slk> zf%wp|MV<i2?N$H>kw!hK*g|YlK2V~KrAJC}1a#x|q$K|U5yc@06yyRr_oZYEpg1QT z4LAS?2Z{j%VA2bbZFu?n!}Y5$%Ny~5{b``?7{^L)mZy?1eML)MN=(v=%Q**+rCdn& z0p+%99UY@Qjlg#mp%kHp<X}x&C6hN?9&u2zsr%xjk>qYqUs{84IR~{2NMpu2sQme; z3Xlog%~)n2aB?a!CQcZG^{8c{n<}vz$4|i2f4z+0VTYmXT163tao4|EfXDNUcBIg5 zLXx9!+#f?y^N*C$G6FWZ2l>={C6E1EZXSk^$-<L@#-v~N4>eV1Iolw?_p2Rw1M#Xv zlZGqQ1MAwHB+o^tS8?O|)PV^Ds5L?-4W6U&G@DVoVEb`U;zb~ied@|ee(R_w*QGHG z8-eMc(A3K?&JIEM;-itmsdK;_oc14D^6gv27h0so+e($9wHz4$!^!kLRDPzitzI;h z<ZD9>I<C<_|JMA=gG06rm4_qstLvpf7$G54J@SA0^>CY<<PD$>$DpcgF65JNARz0z zKd=7)uDza9$h#ROy{I@5tgJhK2>Q~m+4lUXjK`o0@{hu;Bn9Iu^0JP~03U@W#Z<r> zap}pRMMhQ&xG)E}=qgxexs~vUU015B05CiB0-k)Mg}@p08;xFy(pg5;jg?375wv66 z@F{@%ONoOLtL3X4ao_RpUXS5)4KOS79`(b+4Z0B%NEnYpn)J^Mq;YK9&pmO9@OZBt zp@WquK8M@zW)GVe8jT-Mcn?N<#zIC174;^Mpo@4@VBlAZd^QosU>SLSIr`Vq_FyHN zVD+!_uf;6)6AVj@H)VSt*;p8TR{>4a-ba%BY}Lk#@cQjTZ8H3W^38rdU#yq+#~hb) zpTyVcC+%gewVsD-rN-YhpppmsTvx#wMw|o>a^odA2i({6$I;sZ{hh&Y6K{I7oW4s% z`5%CIp;PO5&L0QT5xf5Yx1Ik0BZrFFG^-g=cy(s{>c@m)mL2y}V{`oHKIf0pv-OFK zg9A1^$1NfBQCL#zBF@+3lq{3FgafkY09W5b2VSln#qTD0^>MW6;*{!b1^s_sXHVlj zRi=hWBWw=i-1e^5z_FOLJ80PQV;T13*N!~TC9Jxg??ne{{{VTb*Zc=5lR~$aPOTb~ z_>*5}mSNQu?BlZh&(3q)U$>H~_j`JO!96C!=H&5;>!IyjrHbW%^{%cyBloXav*(dR zRd8c$R}HGGEw<s<9)7jgY9#q&6U!V|GpebQ+h8~WNk4^3(ArYENfn_5?(Mkyu_ivH zkN8)$Zo6x1a~K%Jo&D>`ER%K2On-%AABIV<W}j1z2rc7Y-e+%m+Xp+%hBAtcIQJGW zwX%$n$6sSmNfp@+cl_DHfGAy1g#|J}>w#5X{(msIRzE>ro`;jmEsaY{OOk$PZhF&X zv61;M+#g!2ZGCyPw$sR}GG1OY;4U-oNd)6%xu^Y{G4rwH=dCsD;yElww>6jMU6nm5 z0Jm(ta8I>pk<8=fYqh<o7%Zln<l1qTAC+c(n(eXk9(}50vA8(PH?1*Q8YnE+=Khr% zNoWo^svp_XcVqd}j+i@~`twZ+$gK^{n2CO0UZ$akQMW52?H@7YC#^*WnHYg`M*#Oe zwPqbQ*&_e}$G5E-lc8FDUKDuZRbD|P`wzmP{?|};s2_l<a#=#M=Nu~f3W!4pLcnr( z6j^~qby+2w<x)W4VShT)k5Rf|v@2uSRIot|Ytt303{eq)pp)rZC1W=3Q@5V-MlPYT z&}8DSgLp<Z{d38!Rft3as*XBS!TGl10RELoS1{s=^fQ1!&pg#RqQ7Rug#A=-E2@>( zV8A7ZPSsJJfcf#0=y;)x9NOAk9$rSs&p}RG37LU1kWaoVeWQquaHRXyV+t=Nl=I*J z0M|<8SjYa#k(h$y{xuZ3ULzwQV~q99Zx@6v;LDzdsN7E*j42;bXt=Cl+G%kO%nv8N zD)Q;kD9$oZu;!<VSmRKwlgBs|rr~-8Vm*ZeDVs=g30`VXvc>(}e_DB%upma?T48y* z`DgJofRJ0lGIkDXK!`E&kIJA1=g~p>&_&B0Sw$<1wu+qq9CpPFtB!fWs)1+!0G@z- z1vo~;^CA4GxL9~DNdZO=;Z<%1PERCar7+{GIT`iEM=T1ePD%Qjl9D7uUA&Mzy+2wR zp<m(|sO{oMA9Q_BH9NqH*a{EfiV_03o)O>jrW=$z44<GiYy{iT0x68jzbMB&{b(6m z?ErrZAHtp{m5<92HDhQP0CDS6k)Dm)u*CpUFLJ}>j~Vqf9^0TXh3DUzy1jOTnoXqS z_Z|D1V!2F5a(+_a4&xOYM<g$e_|G{W^`-Lz$j${<oCaKvz3D}P5G+i3$j9kc76FcT z{d2`d1LSSaD?&HRbu`ijF(HHu6(stTR#FI#2n$lj<-f{BBdAx~ilZ^&B~O?rBfkQa z$Ge^dD)gz8oM*p!cLx$B+njVi&ZG)(eqOx|NJBSFeia^CFmuTD^rGYtO9^qv`c-se z8yMRk&aQ?n-;Y|YG6^{x5%^Um7be(q#Xo2q5>K~EsSLRJA33YNS3PK8G=#?>kIJB8 z2i-UoDUY0<ov9I1k>A#VjEUGB9P?I5Am<!Yc5#gHQSXvA`FRw#5nzsUifaMRN8wQk zA0(0i_o+9P$Rl=WwSwg-Vj0~Z<xEE(cb(pqIVS|;r9kRm<{S~;s0RWw(~R>=wGTo? zM6w+0Yy;|QKu<$J2-wXx1pff~>P+YF5lk)#`49D=2NCK?q`(8-lr~g@O+J{Q2{1_K zy$u*64N;cI2dy9N1BwM<F!3C(%)hNjlX;wu-<>eyrfG!(9ynp?(wbznTn^agmv^mF z4pe~szgm<W_n->_SFp}|Q;yyM1XMe=@%Yojf+(<wLFbcFDLrX{kDxiJhCB77GC`m6 zDc(>xWFQ{&z;VWLQiFlotU`H`MZ|+Xgi+?JjDwnx;|DnAskAud4iD*4!4?m($Ec{n zf_WIvda5O4J7pYIp*&0gAOY!`&ia<NAF@Rm3=cIK5@#p#sf)vm?;hZqV_e6Lp|~H7 zR^*Ike#0Zwie~NsoDpO0-M`Oz8feelQctj`{NeuqJlN8(Tnt;fjy?IIV|C>ikFO$) z?%X71gCzLI&_1++<JNMg5^#NcRO=!hN9HP?*<Yz&%9U-{WC!QcsR+I1XRjmjsg<_# z$KzRLT@D<adsAj>Xd!!vUlGQon3K>BO*@6U9QTa%ZzkDv%XY-K9La;yFaF5!^v!Y_ zb+{O;nJ-&XzLxd)^BXEZx}%`{1$34QIIo_>*7lVf>CpO2H6LY7N2&kU{P9~hCkvg- zIv!8y{c3Zk%M@N)DGc1>c7Od<8*3J^{{Y9dEbZL%QTfm>UP2mf{{ZvM7H^=>1M;tJ zSn)mL#CBU3Cz5#~Tlt!-ZS8|^a1XFF`7bS#KWJ>{qUR^+fmH^eBtBFSlmok+tNt{- z0?e@_&_p*RTbT6P1_$YisXnJXi?^u$=sCyaDKM`4ZlwbqI6DXRG^mg(GRG$Do~ky3 z_;X5I7ZP13%(|awQ}_@609|`7hX<9glaevoxIGpq;R6wYo}?Uc`PZvyDG>_cdf@&Q z`L`5e6K*M=uyAG-#+s8y)&Bqu738(@k#bJl4!!H@n+-l;Z4s1U<E40a!5b@Uq*ntD zF`U=f2TN75X|eYZpU7AF!Q$?Eqnk?zm8|dgpV4^S(P8k^Xn9}nKPP``O+mFk8K#{X zC1Aqi>Hh%hub(A_Y&7g2UTfJtF}K>jBI*y4b~zmO=Db(L*DD>Vh#m(&Q(x(Sg?u|p z55l+_5?Uxn=Gs4-_}4OV&oMP`s&-zBL+9O2<)xP6B;$F{)3E#na+Y>yTGs9DkTD4& zvw8(yYo?Mi=48fmA^b@uy4yPlv<*3@WD9k6xnqu1Kl=6cJV!#74i~1q>1e;t<aqgf zCyt-Ato_xi{TIyP{6lpUhO@UnxwlfI+>WNbm%u=`hix;*?$`O6@!f3O+_aKL08l!C zjE{Qu&xgzA=r+BEz<(lZhX-W^8+1NnG3Iq;IXmn9eaENS3^MWBx-pQI&sZ^O0siQ& zk8+jCuDwq_%GwwjlPCpi0`M0bM@C$KD%;e0lj~VCjN=}dsHrPOvwS>nG>2J*`$x;$ z%3sqz*1djPX#US`4=4AZ&b)@$fpsYyfh1$`3M<(G-ERkT`_KIfwaZqrV$Ee`6d<;V zBLohIBl4+I-Y}<u&#g{Z_k8_n#AQx+;=M!3L_!$h1Y?S<RR=f#pK7#QAdcVCtG)>5 z@TQ2U^djh|r&?eG7$=GeIKbncwBe7>CamWgI3v=WTLbG%&M;|=O#&1eMs6viflXtd z!kPm%CO%q@$2qA(f&liau@U9xJt^qi1JgBPBb|p{nFRioLK0gC1ml5I-IKc>tAY5^ zz_?zS;+jOVUGp<wMn!1A*J+KYquiQ`2#k$``qsQrmEhrV-#u!{8NT<DT@#;rjKy|N zeB~YZtp<oN`G`38$LUqs7aVh%8WLr51kMAz>{NORiL8S-9%KC~>bndgfsU9wRQrT! z&NKSbD&-&S_(A!Vn4ZO{$*08G1x9~5whno09QxBX1IRpjQeZ$V;vE!V4r?y)7BxFK z&lR|UV3Ec<eJdX5ZhU+GG$0!cY@lbJIL%6{j!3}tr$ZQ1w>*w&Qn|nfKE0{j3eFEX z=}>IrjMjs~ag{Y(s{x(VTy`oEwDl&F8K<`+G=&dSidO`Nk&NP#b`CL5D97A%rUb$H zX%vu*&C@+;jt1Nsk(BhuT4OH<*U-`#TG-%`pKq;QWeg5F`qq4ZEQdWaR|KEKjQf*7 z265|xdQ(pXmK|zEZZp(+(0RchtpG47<7vn8r+~+jN79sndFP&nlqkjtB7hlyxyO2T zMkv7T(v*>cd-_ul-V|~Eb5UhT$MmWCoSu8)kbro`O#(B1<es4StLktA3W9XV`cq(j z)W>r{C1v@C9@M^LG0i|6W2SLW{t<zScO|0Qu_uF0Sr1I$`}eBK+2g;ZID$_c;+Tyn z-GJ=Eq$LRB1M6AoF4fN5el;kNws{<M#TF%!6$7>hwF4h^ndgT2S$ccYES*CNt1%E< zM9SNcgO5r`re?yE)7qR_`5EG&D9Q7$<5VKqB=sMSG>7N_KaC<wyb5ptf;-SL$-5YC zJ^Io}IN<R{@~52sCYnhr!KGqI!!hK3qO2_BF4qE~+^2)qo4dAutuc;w%WyjSRDU*d zKPpxv8XKq};Lrqand{fJMZBlV<MFA9Us3g_-J?Cl06*p<IH-m}sDtc1awsyK@H0aK zb0FYj7@)R$@m06CP==H|naEK<77GmZrOrpSR?MX5fkxRsh~j`O@~0!3P{X5fRkk*H zV@u_!>PetQhVqEULHueB-dW&+Jt;i3r3k$+XcdV>$=;pkARK2uN`%IGjtw~+uMR&N zcNK}!aybf3N-iYGAt|Uq`@GeKjz;^c$LKRo;vq9!6Of=&gqJPX0<OnndimJ|hac>k zwH}MAxx(8p2iJ;M9hsWGasL38n4`;f7%~&|t<U&JwM=D|BLn-)1w43q;ZN@_VP5|L zdo>NEyBdQz_P31v^2DEdL2jK>^{$Wm7TrhhFBJa(x`fl0P1DfxJdKY<z&}iy&D4i; zRZ2~4bI7Qt5+=fbBUe^5#{eI3TMRS^hwnE&I_{B1KLJsVQ$V*RQW(#sT(JEGA6TZk zht=vng#fAtr{O_N{m@Nc{{V!aOpgXTc!oO&$v>$TUHo2%PmviO;Sf@Is$*D6jEGxK z+$1h@=nwc*i=^MkGPdvdimNA#FIZ(&`RnuJ^a8T3JXv%{&gg)2-sI!+HJio9p`yW0 zsnLxOQR+}A9d|GosZ&IhW6hov&#Qy{tIp=}v{4+%dv3m<V3L1Y&6CGavD&wCKl_M( zps9Gp*wJ8LR(jylEtC@XX~$(!YtMco{fkY}5(`;cNp9r`eqEzt{LTD;{A-^+DW2>7 zRCm)8{n*%lQCxS3udXicpoVxRfTEV$H{Lt3&NJ;?^|5Wiqk1?vsiw%RMloHDpFnGn zwc&H=U0sNhKnkff<{qOz|Iqy7n`UwbK<S<ZSW7$mwA`o!W2jOTW8CJl-MqHw81>6@ zSB=!d-CsE;vFJYv_Uw4O3|LGKLBwSDUzv~MdVUoD05OV{js<4vy@ql0s|bODDJS=w zo@yC~mI+dRUV!wfw{UFFm57idg>rZRagWOtYA1AHFseJ_wN_OUHs<P0OEQQb3Db@Z zb60vh9W*V>n>!sk<7hczOJFapdtQuGMIPgp<Bs*<8YwSv4(@r)eFNd76EcVT*W`R> zRnbZ@-JiJdL{(+Z_F5lp`~lpojCvNueWR^hM`NH|Oy>j2Kd7&eehS2qq<eVzg?$t8 zUhaEe22Rkn35~P)SNX}}HeT_(XG;p-@g)@Bt3O`N=qciGl)71d8lRB=01>qmyzzFY zaG&zhX2;VeyhB}oIzb*w0i2(D^dAt)@2Xx($1Ua_Ylxjg2UFkk=D*mSiwO)SUq=rQ zh7n8fKa)6<8$3p3R&x7OZ}B|yMAFh-LVLT8AcfeB_rd=FKaFGf$60SJ$T%<Zf#?T8 zU7n_X%Wly@@~b(|{0F6YpNgiDuU0aCRZu|pHSoC1Y_a&qT3RKdUWeOPqOe#VNN=V6 zSv}OG+c_YD>&1Kj0K;$j7j5&}FY0UO>xL}>W7lnc6Y$6b;kYyZ06fqBglp8Hy^qf^ zOG}=GU_n1GUMr?Sx-(qno&YE7T@WQ#is);h$j)n2Y!JQkR3~5nUjG0u>T33)pt|R~ z3d_4E&QJG{e>%#pu{!Xd8bZLqZten(lc)ZIwe6PR7;Vq9{(GzE>nR=e8{o%<y&!!C zE9fYAw}AUe{{W#^USAtUvyU{L>Lgn{u04O11pV))6pP3{^v~kYsIKFJ<S`OgapNcS zsH8YJ6*fs+)D6~}Gm{};agS5Y8RLp`^vy(nsjDy~Zh%w{a55@J>7QzahX$A-o47RG zkPkl9S(xtwKgEiYQ;cKYs=#K%K}>b#rA%XS-~&-b#A*k8{{T8yB~n5=3IMMm^JgVT z1A+Z(LNwqCr*HhLW)%nVtr9aWLV*`2f;}o?&9@;xI+-I;wMIZS0y{kW8>VTB<hYv| zKPV$Op%WmpWag~k1p1nSMj?3jrqG5*<*S{;C*Rhmk)YfFAfEZ@RYHN!T=UIN0x*A( zeq7^<L$$>&t$~1l`qZwVV2l#nWMlbM3Zvy6k7{H5qXWH581c6xe_G5SH2CN$@B;+n zwN!yaT?9Q1FeY?D;g^oYQW41ashvPOfW|2p;A9Gx1X;)c?g#0buG>)UZlb3$Gai6_ zDTkK9=|IsQ7Z^DoQB~NT*e4#D7^_XlJ+}0!=cexYqo~;!!RUDO2Ao)^#@dg*4NUxJ z1EHW~erY47Jt>8<K)}iCPbBUmwML+3fn1>D66B24vU7vi6%?Zq52>k>wt1v9C_TyR z+JTIn$FHHMGN>8A&T&!aoO)0)U;!SvqzXS8MC6>(;8PP913hUlMn_+Ie?T$mP0kol zCMH)ug&hwDhT@J77@!cxJRIX6t!I(3@;I#)=ZdE=1ZTLS<0Xui<JPAN!vW4n;0mOK z?Z~MEv0^B=ShlQHxZ~2S2V;%>J*zRNd}oY-QGKRNj+E0AHt?;=W|VR{=CamF4<(IG zrN6vt?q<uoPCC)KM^Y-N9CbA1Mf9q<C7&`e{6eE?BgO&9`cM%;98|2T4+5AJ@<+@w z#Y!a4J8HwIBRK6<lY@_?04`sutp%kgfCWl=W{^+>C1xiikHVYfu{qz5rC7yA-OT_r zMe4+lY6NMXrxh?H42~*zM&dKv;(#joN!U|1CU_Oo+UZu(u~#kl5&jj@TX<hfbqwM{ zf8PH9>r~un9&BPB)fbk?cMe5+g};RKPVkocwrbqE9+7FV$7vZPk@F1nsdW+`D?A)1 zVYzB$w4L#`H$U(bUdnu!*_JDKP-n6VM!A$@Zq?_qf&D8dT_Z(B_dJy}twKy~wsL-$ zHF-QWtjW*WSw=gNUYcZ(F73zj#d129w|fMPskeegPD$YXYF;MY3yvQBOqB4Bs=zad z<M%CASHctX=H0@W^ya9YZ%}jhm3@Mb>zc0GrlE{^?Z>7E@~mDn$kkx))ZI@CTPXrt z%#Qn1{{V$p9v#u6A3U=+<h97lb#%uKG>1KM#-cwrCqK@m<0a7)u#rb*Y<w|pa^rS5 zJEb4Xns3@XGZ)IRLQlCZ{Dpb#)XfSrC<K5{HRqbYi*-vwh*lBxQh%*O#m$;5Brjv= zZ|%(*BbcE;eFQ|G*A*Lht`!)K7-Oc?QC}~wi?!Q~6F5wqe5yaLYj(opQNL}o-&_EH zxWRvrsQS%E)W531bUof*7Tllpak&SsUmwU;Y~Crl>g5;EvlD^%n&RzzF=cWAxVXEM zJ##VsmC;*x55q)eOLma|0Ji17ogDLhN9^Fb6|Q__kssb&z`nz2{{R}uo5wbXgL=*X z09@4n0PLc>MevV@VHisohrT(hF=)DEBjvY4?o@w;7MF6LD(HEe_|7RwSd#8ieGv2c zkxpG#Qo0}L`d#Fsw(KbWb?h<NT1Ej#kRCcH{{RYEK32l;DY|}Or+ty+YY&QM$CIS0 z1OEU$CCBm=I_RD=m#SI8pIy!UYt)G$NzFzN8KvyR=F#S7!agaR9&V=;W70Ul{&}az z;7j)h?RvezKkR(J%N6NGh%}1Dn1fWHiS~@))8Y1%Nc&CA)Wr4OD`WZ9#PBDD;y>}} z<`Lc4xAU&nKmZuyAR2^G%7xC(c|0rO=y^*RhJC8^I!2WlJ1wJJbi#_$JdS-Sm;<Fd zXe+QtB0-4IFVF#BJb32g?H(+<9Yg{+MnBn5PwH#vc_n1|R~R+&*0Cc?uj&#x%Nv<< z_%W<2zjWUZ@iSLUh}=w(j)e2{uT6#(i2~pP1#zAgi5ZKL&p)S1?xP!<n&wF9lRy92 Dv`J~z literal 0 HcmV?d00001 diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index 3a462ec..a784f11 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -643,6 +643,13 @@ Digimon Story Cyber Sleuth: Complete Edition (Media.Vision's MVGL engine, SDK 6. - **Found on:** [Digimon Story Cyber Sleuth: Complete Edition](../../dm/games/010014E00DB56000/README.md): `main` imports `nvnBootstrapLoader` and 13 EGL functions. Its graphics setup (`main+0x5ed8f0`) makes an NVN device first, which only a helper beside the movie player uses (`main+0x528000`–`+0x52c400`: a memory pool and four textures next to the movie's GL textures). Then `eglGetDisplay`, `eglBindAPI(EGL_OPENGL_API)`, a 4.5 core context, a window surface on `vi::CreateLayer(w, h)` and a glad loader (`main+0x52c460`). Of the 22 games extracted here (at the time), only Digimon matches the rule. [Balatro](../../dm/games/0100CD801CE5E000/README.md) (SDK 18.3.2, LÖVE 11.4 on OpenGL with SDL and LuaJIT; one boot): `main` imports the EGL functions and `vkGetInstanceProcAddr`, SDL's Vulkan loader, which nothing calls (`subsdk1` is NVIDIA's Vulkan driver); Hades and both Galaxies import 99 or 100 Vulkan functions, `vkCreateInstance` among them. - **Note:** [Absolum](../../dm/games/0100A83022A22000/README.md) imports the same EGL functions (SDL3) but renders with NVN and names no GL function. +### NVN in NROs under an SDL main that names GL + +- **Symptom:** the NSP script took the game for an OpenGL one (`main imports EGL and names glShaderSource`), the GL shader capture found no shader, and the capture boot stopped with `[nvn-mtl] FATAL cannot read the shader map` from `h_nvnQueueSubmitCommands` called in an NRO (`nro8+0xc10d0`). +- **Cause:** Portal's `main` is SDL with its EGL video backend; it imports 20 EGL functions and names `glShaderSource` twice (SDL's GL renderer), but imports no NVN and draws nothing. The renderer is in the Source engine NROs: `materialsystem.nro` (with the D3D9 layer linked in) and `video_nx.nro` import `nvnBootstrapLoader` and draw with NVN. +- **Fix:** `profile_guess.py` `shader_sources` now reads the imports of the loose NROs in the RomFS (`romfs_nro_imports`): when one imports `nvnBootstrapLoader`, the game is an NVN game whatever `main` imports. Digimon keeps the GL rule: it has no NRO. +- **Found on:** [Portal](../../dm/games/01007BB017812000/README.md). + ### Which GL functions a game calls - **Symptom:** a jump to address 0 right after `glGetFloatv(0xb12)` (`lr=main+0x2d58c4`). diff --git a/docs/reference/services.md b/docs/reference/services.md index d5cce6c..ab4b107 100644 --- a/docs/reference/services.md +++ b/docs/reference/services.md @@ -588,6 +588,20 @@ Most service calls can be found before they crash, before the first boot or the - **Fix:** they are never loaded (`profile_guess.py` `GPU_DRIVERS`); the EGL imports stay unbound stubs that nothing calls. - **Found on:** [Absolum](../../dm/games/0100A83022A22000/README.md) (FNA3D). +### Modules only the NROs import from + +- **Symptom:** `UNIMPL _ZN5movie12SetAllocatorEPFPvmmS0_EPFvS0_S0_EPFS0_S0_mS0_ES0_`, `UNIMPL glslcInitialize`, then `UNIMPL _ZN5movie9ExtractorC1ENS_13ContainerTypeE` and a crash reading address 0x18 in an NRO (`pc=nro18+0x97f4`) as the first movie starts. +- **Cause:** the provisional profile loads a subsdk only when `main` imports something from it that the host does not supply. The survey extracts only the code modules, so the NROs in the RomFS were not looked at. Portal's `main` is a launcher with SDL; its Source engine NROs play the movies through NintendoSDK_movie (`subsdk2`, imported by `video_nx.nro`) and initialize and call GLSLC (`subsdk0`, imported by `materialsystem.nro` and `video_nx.nro`). Left out, their imports were bound to stubs, and the stub constructor of `movie::Extractor` left the object empty. +- **Fix:** once the rewrite stage has collected the NROs, the NSP script adds to a provisional profile every subsdk whose exports an NRO imports and that no loaded module, other NRO or host hook supplies (`profile_guess.py` `nro_modules`, written under a section `After the NRO collection`). NVIDIA's GPU drivers stay out, as above. +- **Found on:** [Portal](../../dm/games/01007BB017812000/README.md) (`modules main subsdk0 subsdk2 sdk`; `subsdk1`, NVIDIA's OpenGL driver, stays out). + +### `ro::LookupModuleSymbol` for a name the module does not define + +- **Symptom:** `[RED] ro::LookupModuleSymbol BuiltDebug: not defined in that module` once at boot; nothing goes wrong after it. +- **Cause:** the Source engine asks each module it loads for `BuiltDebug`, which only its debug builds define. `ldr:ro` fails such a lookup on Horizon too, and the game takes the failure as "release build". +- **Fix:** the host's answer was already Horizon's (a failure result). The line is no longer red when the module is a loaded one; a lookup in a module the host does not know stays red (`hle_ro.c`). +- **Found on:** [Portal](../../dm/games/01007BB017812000/README.md). + ### IL2CPP common string table intact - Not needed in the end: the tolerant common string table of pull request #15. [Ace Attorney Investigations Collection](../../dm/games/010005501E68C000/README.md) 1.0.0.2's `main` has the table intact at `main+0x46169ac`, 109 entries ending at `Hash128`. diff --git a/docs/reference/shaders.md b/docs/reference/shaders.md index 7cfe918..c62a366 100644 --- a/docs/reference/shaders.md +++ b/docs/reference/shaders.md @@ -66,6 +66,13 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b - **Fix:** such an output is read by its section headers (+0x4c section count, section *i*'s header at +0x90 + 0x90 *i*, type 0 = GPU code; `glslc_program`; the same headers SDK 4's outputs are read through, [GLSLC outputs read through their section headers](#glslc-outputs-read-through-their-section-headers)), and its stages are paired with each other, not as single-stage programs. The NSP script now stops at the split when it finds none. - **Found on:** Baba Is You (no profile). +### GLSLC outputs in a zip archive, and those NROs compile at run time + +- **Symptom:** an NVN game's split finds none of its shipped shaders, and the ones it compiles at run time were not captured: the game's renderer is in NROs that call `glslcCompile`, and its whole RomFS is one zip. +- **Cause:** Portal keeps all its files in `game.zip` (41,416 members, Deflate or stored). The Source engine's D3D9 layer (`materialsystem.nro`) links each vertex and pixel shader combination into one program; the programs of the game's precache list (`portal/glbaseshaders.cfg`, 219 entries) ship compiled as `nxcontent/nvsb/<hash>.nvsb` (`NVSB`, version 0x65, the file's length at +8, a 0x60-byte header, then one GLSLC output holding a vertex and a pixel shader). A deflated member hides the GLSLC magic from the loose-file scan. The game compiles the rest with `glslcCompile` (its movie shaders and its own blits: 10 outputs within 30 s of boot), and the capture rule looked only for `main` holding GLSL sources and importing `glslcCompile`. +- **Fix:** `shader_split.py` reads zip archives member by member in parallel ranges (`zip_jobs` / `zip_shaders`; `zipfile`), each member scanned like a loose file, whole GLSLC outputs included. `profile_guess.py` `shader_sources` asks for the run-time GLSLC capture when an NRO the game loads imports `glslcCompile` and a subsdk defines it (`glslc_capture`). Portal's map: 242 keys from the `.nvsb` files and 13 from the capture, 229 vertex and pixel pairs. +- **Found on:** [Portal](../../dm/games/01007BB017812000/README.md). No other profile's RomFS has a zip archive holding shaders. + ### G2S containers in KTGL RDB archives - **How it is stored:** each material's shaders are a G2S container (`_S2G3000`, type KTID `0x7bcd279f`; 23,108 entries in `asset/system.rdb.bin*`, 2.3 GB inflated) of GLSLC outputs, as in 13 Sentinels' `main`, inside zlib-chunked IDRK entries ([01](../agents/01-extract.md)). One `_DOK0000` container (type `0x20a6a0bb`, the engine's system resources) holds 151 more bare GLSLC outputs, 21 of them drawn in the first battle. diff --git a/scripts/build_ipa_from_nsp.py b/scripts/build_ipa_from_nsp.py index fa6f9cf..4634f0e 100755 --- a/scripts/build_ipa_from_nsp.py +++ b/scripts/build_ipa_from_nsp.py @@ -876,15 +876,28 @@ def collect_nros(py, repo, romfs, dst, env, logs): return {l.split('\t')[0]: dst / (l.split('\t')[0] + '.nro') for l in (dst / 'nro.tsv').read_text().splitlines() if l and not l.startswith('#') and not l.startswith('refused\t')} -def rewrite(py, repo, exe_dir, out, env, logs, g): +def nro_modules(g, exe_dir, nros): + """modules of a provisional profile once the NROs are collected: the subsdks the NROs import from, which the survey could not see (dm/games/profile_guess.py nro_modules; Portal's movie + module and GLSLC). Returns the profile, reloaded when it changed""" + add = pg.nro_modules(exe_dir, nros, g.words('modules')) + if not add: return g + mods = sorted(set(g.words('modules')) | set(add), key=pg.load_order) + why = ', '.join(f'{m} ({k})' for m, k in add.items()) + say(f'modules {" ".join(mods)}: the NROs the game loads import from {why}') + return set_keys(g, {'modules': (' '.join(mods), f'the NROs the game loads at run time import from {why}, which main does not')}, 'After the NRO collection') + + +def rewrite(py, repo, exe_dir, out, env, logs, g, provisional=False): """patch_nso.py on every code module (each writes its own <module>.* files, all the same build_src.sha256 line), each with its log, as many at once as the Mac has cores. - The modules are the profile's and the NROs the game registers (collect_nros; nro.tsv beside them lists those). Reused when the modules, patch_nso.py (with nso_code.py, nso_asm.py - and the selected profile's function rules / the patch files of their builds), its switches and capstone are as at the last successful rewrite (Monster Hunter Rise's main alone takes 5.5 min)""" + The modules are the profile's and the NROs the game registers (collect_nros; nro.tsv beside them lists those); a provisional profile first gets the subsdks those NROs import from (nro_modules). + Reused when the modules, patch_nso.py (with nso_code.py, nso_asm.py and the selected profile's function rules / the patch files of their builds), its switches and capstone are as at the + last successful rewrite (Monster Hunter Rise's main alone takes 5.5 min). Returns the profile""" import capstone - mods = g.words('modules') PROG.stage('rewrite', step='checking the inputs') nro_dir = out.with_name('nro_' + out.name.removeprefix('patched_')) nros = collect_nros(py, repo, exe_dir.parent / 'RomFS', nro_dir, env, logs) + if provisional and nros: g = nro_modules(g, exe_dir, nros) + mods = g.words('modules') manifest = nro_dir / 'nro.tsv' src = {m: exe_dir / m for m in mods} | nros names = list(src) @@ -892,7 +905,7 @@ def rewrite(py, repo, exe_dir, out, env, logs, g): key = fingerprint(('files', [repo / 'dm/tools' / f for f in ('patch_nso.py', 'nso_code.py', 'nso_asm.py', 'nso_match.py')] + patch_files(repo, exe_dir, mods, env.get('DM_GAME')) + list(src.values()) + ([manifest] if manifest.is_file() else [])), names, capstone.__version__, {k: env.get(k) for k in ('DM_PATCH_FRONT', 'DM_GAME')}) if all((out / f'{m}.patched.bin').is_file() for m in names) and reused(kf, key) is not None: - PROG.end(f'{", ".join(mods)}{f" and {len(nros)} NROs" if nros else ""}, reused (inputs unchanged)'); return + PROG.end(f'{", ".join(mods)}{f" and {len(nros)} NROs" if nros else ""}, reused (inputs unchanged)'); return g tmp = out.with_name(out.name + '.partial'); rmtree(tmp); tmp.mkdir(parents=True); logs.mkdir(parents=True, exist_ok=True) if manifest.is_file(): shutil.copyfile(manifest, tmp / 'nro.tsv') # refused-only manifests preserve the game's offline registration path too PROG.set(step=', '.join(mods) + (f' and {len(nros)} NROs' if nros else ''), done=0, total=len(names), unit='modules') @@ -916,6 +929,7 @@ def rewrite(py, repo, exe_dir, out, env, logs, g): for f in logf.values(): f.close() rmtree(kf); swap_in(tmp, out); kf.write_text(f'{key}\n') PROG.end(', '.join(mods) + (f' and {len(nros)} NROs' if nros else '')) + return g GLSLC_SECS = 30 # the longest capture run: Three Houses compiles its 9 shaders within 1.2 s of boot, and the run stops as soon as every file the profile names is there @@ -1770,7 +1784,7 @@ def main(args=None): + (f'; the game\'s languages: {" ".join(have)}' if have else '; the control data lists no languages (not checked)') + (' (DM_LANG=<code> in the run command changes it)' if mac else ' (DM_LANG=<code> in the app\'s Documents/env.txt changes it)')) patched = work / f'patched_{pkg_ver}' - rewrite(py, repo, game / 'ExeFS', patched, env, logs, g) + g = rewrite(py, repo, game / 'ExeFS', patched, env, logs, g, provisional) chain_header(py, repo, env, logs) # before any host is built if provisional or derived: # the keys that need the rewritten modules, then (provisional) the heap check (both only change keys the host is built with, not the rewrite or the shader steps) provisional_prep(py, repo, env, logs, g) From 6b947d950405629a8dd563f64482e3c76a2540b5 Mon Sep 17 00:00:00 2001 From: seedds <seedds@gmail.com> Date: Mon, 26 Oct 2026 01:46:07 +0800 Subject: [PATCH 6/6] Fix Xcode workflow mock for the rewrite profile return Return the fixture profile from the rewrite mock so the preparation pipeline keeps a valid game profile after the Portal change. Validation: all three tests in dm/ios/tests/test_xcode_workflow.py pass; git diff --check passes. --- dm/ios/tests/test_xcode_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dm/ios/tests/test_xcode_workflow.py b/dm/ios/tests/test_xcode_workflow.py index 98f6565..7e2f784 100644 --- a/dm/ios/tests/test_xcode_workflow.py +++ b/dm/ios/tests/test_xcode_workflow.py @@ -53,7 +53,7 @@ class XcodePipelineTests(unittest.TestCase): 'scan_nsps': (int(registered.id, 16), fam), 'check_prereqs': None, 'free_gb': 10000, 'build_nx_extract': root / 'nx_extract', 'extract': extraction, 'can_clone': False, 'build_mismatch': [], - 'extract_aoc': None, 'rewrite': None, 'split_shaders': 'fixture-key', + 'extract_aoc': None, 'rewrite': profile, 'split_shaders': 'fixture-key', 'build_shaders': root / 'shaders', 'build_vk_shaders': root / 'shaders', 'build_gl_shaders': root / 'shaders', 'provisional_prep': None, 'chain_header': None, 'refine_after_rewrite': profile, 'derive_profile': '1.0.0', 'run': None,