Pipeline prebuild: the pulling base of every known shader pair into the pipeline archive, with the pairs of agl's BAHS program variations (pso_pairs.tsv)
A pulling stand-in is used only when its base is made or archived, so a cold start's draws waited until the prebuild puts the base of every known pair into the archive (pso_prebuild): in the app after the prewarm, on a utility queue on half the performance cores, flushing every 1,000 bases (DM_PSO_PREBUILD=0: off), or as a tool on the Mac (DM_PSO_PREBUILD=tool: build, write, exit; DM_PSO_PREBUILD_DEPTH=zero, DM_PSO_PREBUILD_THREADS=<n>), once per chain version, depth mode and pair list (pso_archive/prebuilt.tsv). The NSP script runs the tool in a --platform mac build (pso_prebuild.log). Files that hold only bases without a vertex layout are named p<seq>.mtl4a; a base with a vertex layout is never looked up in them (each file a lookup misses in costs about 0.06 ms) and compaction leaves them alone. The pairs: shader_pairs.tsv paired the stages of one BNSH variation and none of the pairs in agl's BAHS shader archives (.sharcb: the agl / gsys / terrain resource packs), and in Breath of the Wild 109 of the 543 pairs drawn on entering Zora's Domain came from those, so they had no stand-in. shader_split.py now reads a BAHS file's binary array (record size, kind: 0 vertex, 1 fragment, 3 compute, the entry bahs_shaders reads) and program array (record size, name length, stage mask, first binary index), and pairs the n-th vertex binary of a program with its n-th fragment binary (bahs_variations); a file whose arrays do not read back gives none. Those pairs go to a new table, pso_pairs.tsv, which only the prebuild reads (DM_PSO_PAIRS: another path): vs_pad keeps reading shader_pairs.tsv, so no shader library, inputs digest or other table changes. Breath of the Wild: 3,012 more pairs, and all 543 pairs drawn are now known; its shader_map / facts / pairs tables split again byte-identical. A profile made before the table existed still packs: the NSP script compares pso_pairs.tsv only where the profile has one, and otherwise keeps it beside the shader package and hands it to the prebuild. Seven other profiles have BAHS files (Odyssey, Wonder, Mario Kart 8 Deluxe, 3D World, Animal Crossing, Captain Toad, Tears of the Kingdom); their tables are made at their next pack. Breath of the Wild, Mac (M2 Max), Zora's Domain save, empty Metal shader cache, no pso_user.tsv. Prebuild: 15,767 bases, 910 MB in 16 files, 187 s with 7 workers for the first 12,755 and 19 s for the 3,012 more; the files open in 2.2 s cold, 0.25 s from the file cache, and add about 850 MB of footprint. The frame entering the area: 340 ms with pso_pairs.tsv, 830 ms with shader_pairs.tsv alone, 990 ms through the MTLDevice (then frames of 738, 535 and 496 ms); draws that waited: 50 (20 ms), 102 (650 ms) and 512 (3,325 ms). The picture is the same. Tests: t_pso_archive (41 checks: also a base without a layout flushed into a p file, and a base with a layout not looked up there); the other graphics tests, the host unit tests and test_nsp_mac / progress / chain give the results they give on main. Not run: an iPhone (the prebuild is off there with the Metal 4 path), the NSP script's pack end to end, the other BAHS profiles' splits. docs/development.md, dm/gfx/README.md and dm/games/README.md describe the prebuild and the table; Breath of the Wild's note (dm/games/01007EF00011E000/README.md, Pipelines on a cold start) has its measurements.
This commit is contained in:
1 parent
8593d85a9c
commit
e0ef4e707e
10 files changed
+3268
-39
No files matched your search
@@ -102,6 +102,20 @@ Used at `DM_FPS_TARGET=0` and for the interval the framework keeps while the VFR
|
||||
- **Resolved: duplicate pipeline keys from disabled blend state.** A recording held 4,829 keys for 1,982 distinct pipelines from 1,818 Metal 4 bases; 2,845 keys differed only in blend factors of targets whose blending was disabled. [Pipeline keys omit unused colour state](../../../docs/reference/shaders.md#pipeline-keys-omit-unused-colour-state) describes the shared normalization.
|
||||
- `pack_ipa.py` packs its 16 GB IPA in 57 s (Info-ZIP took 2 min 50 s).
|
||||
|
||||
### Pipelines on a cold start
|
||||
|
||||
- `pso_pairs.tsv`: 3,012 shader pairs from agl's BAHS program variations (the agl / gsys / terrain resource packs), which `shader_pairs.tsv` does not hold. 109 of the 543 pairs drawn on entering Zora's Domain came from them; with the table all 543 are known. The prebuild (15,767 bases) takes 910 MB in 16 files, 187 s with 7 workers for the first 12,755 and 19 s for the 3,012 more; the files open in 2.2 s cold, 0.25 s from the file cache, and add about 850 MB of footprint.
|
||||
- Mac (M2 Max), the Zora's Domain save, Metal shader cache empty (a new bundle ID per run):
|
||||
|
||||
| Start | Frame entering the area | Draws that waited, entering it |
|
||||
|---|---|---|
|
||||
| `DM_PSO_MTL4=0`, no `pso_user.tsv` | 990 ms, then 535 / 738 / 496 ms frames | 512 draws, 3,325 ms |
|
||||
| Hybrid, archive prebuilt from `shader_pairs.tsv`, no `pso_user.tsv` | 830 ms, no other frame over 250 ms | 102 draws, 650 ms |
|
||||
| Hybrid, archive prebuilt with `pso_pairs.tsv` too, no `pso_user.tsv` | 340 ms, no other frame over 150 ms | 50 draws, 20 ms (no pair without its pulling base) |
|
||||
| Hybrid, archive and `pso_user.tsv` of an earlier run | no frame over 100 ms | 17 draws, 4.6 ms |
|
||||
|
||||
Every kind of cold start also shows the game's own 130–220 ms frames every 9–13 frames for a while after the load, with no pipeline work in them.
|
||||
|
||||
## Mac routes
|
||||
|
||||
- From the title menu into the autosave: `DM_KEYS=f2500:A,f2800:A,f3100:A,f3400:A` (control at frame ~3900).
|
||||
@@ -119,6 +133,7 @@ General host changes first needed here; each applies to every game:
|
||||
- **Spin waits that take a spin lock** (`patch_nso.py` "Spin waits", `dm_spin_wait`) and **yields that make progress** (`thr_yield`): [development.md, Power](../../../docs/development.md#power).
|
||||
- **`DT_RELR`** (`relr_apply`) and **halfword jump tables** (`half_count`): [Packed relative relocations (DT_RELR)](../../../docs/reference/rewriter.md#packed-relative-relocations-dt_relr), [Halfword jump table landing pads sized to the largest entry](../../../docs/reference/rewriter.md#halfword-jump-table-landing-pads-sized-to-the-largest-entry). **NVN calls on the big host stack** (`nvn_big_entry`): [NVN entry points on the big host stack](../../../docs/reference/nvn-objects.md#nvn-entry-points-on-the-big-host-stack).
|
||||
- **SDK 20 service calls** and **record sizes against `GetCommandMemoryUsed`** (`draw_rec`): [SDK 20 six-axis sensor and amiibo (`nfp`) calls](../../../docs/reference/services.md#sdk-20-six-axis-sensor-and-amiibo-nfp-calls), [Applet resource user ID, launch parameters and VR mode](../../../docs/reference/services.md#applet-resource-user-id-launch-parameters-and-vr-mode), [Shared font load request](../../../docs/reference/services.md#shared-font-load-request), [Model heaps sized by the host's command record sizes](../../../docs/reference/memory.md#model-heaps-sized-by-the-hosts-command-record-sizes). **SDK 20's audio renderer layout** (`render_once`): [SDK 20's `AudioRenderSystem` layout](../../../docs/reference/audio.md#sdk-20s-audiorendersystem-layout). **Running a 30 fps game at 60**: [sead's dynamic present interval, `ksys::VFR` and the host's frame step](../../../docs/reference/frame-rate.md#seads-dynamic-present-interval-ksysvfr-and-the-hosts-frame-step).
|
||||
- **Pipeline bases, hybrid pipelines and the prebuild** (`pso_build_ex`, `hybrid_get`, `pso_prebuild`): [development.md, Hybrid pipelines](../../../docs/development.md#hybrid-pipelines), [Pulling vertex inputs](../../../docs/reference/rendering.md#pulling-vertex-inputs); measured under [Pipelines on a cold start](#pipelines-on-a-cold-start).
|
||||
- **Hitch dumps and guest thread names** (`DM_HITCH_DUMP`), **heap probes** (`DM_PROBE_HEAP*`), **autorelease leaks**: [05, Scripted input and thread scheduling](../../../docs/agents/05-debugging.md#scripted-input-and-thread-scheduling), [05, Memory growth](../../../docs/agents/05-debugging.md#memory-growth).
|
||||
|
||||
## Open
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -3,7 +3,7 @@
|
||||
Each game the port knows has a folder `dm/games/<id>/`, named after the base game's title ID: Mario's is `010015100B514000`, Engage's `0100A6301214E000`. The folder holds:
|
||||
|
||||
- **`game.tsv`**: the game's constants, one `key<TAB>value` row each, with comments on their own lines.
|
||||
- **The shader tables** that `shader_split.py` writes: `shader_map.tsv` (+ `.conflicts.tsv`), `shader_facts.tsv`, `shader_pairs.tsv`.
|
||||
- **The shader tables** that `shader_split.py` writes: `shader_map.tsv` (+ `.conflicts.tsv`), `shader_facts.tsv`, `shader_pairs.tsv`, and `pso_pairs.tsv` (the pairs of agl BAHS program variations, read only by the pipeline prebuild; a profile without it still packs).
|
||||
- **`shader_facts_manual.tsv`** (optional): hand-written translation facts in `shader_facts.tsv`'s row format, for what the machine code cannot tell (Link's Awakening: a vertex input the integer-input guess gets wrong; Breath of the Wild and Tears of the Kingdom: integer fragment outputs, which a game without a PC version has no other source for). `shader_split.py` merges it over the generated rows; since the table is on the chain, changing it changes only that profile's chain version.
|
||||
- **`pso_manifest.tsv`**: the prewarm seed list.
|
||||
- **`vk_capture.tsv`** (a Vulkan game that generates its shaders at run time, Super Mario Galaxy): the keys of the SPIR-V modules it generates.
|
||||
@@ -175,7 +175,7 @@ These are not profile keys: the tools read them from the game files each time, b
|
||||
|
||||
The shader tables, `c1_full.tsv` and `cb_extent.tsv` are written into the same folder. A second run reuses that `game.tsv` (delete it to survey again), so it can be edited by hand. `--provisional` builds from a provisional profile even for a game that has one in `dm/games`, to test the guesses: `DM_GAMES_FIRST=1` makes every tool search `DM_GAMES_DIR` first, and the IPA gets `-provisional` in its name. Tools find profiles outside `dm/games` through `DM_GAMES_DIR` (folders separated by `:`, searched after `dm/games`); `make -C dm/host` reaches such a profile through the symlink `dm/host/gen/<id>/profile`. To keep a game, move the folder into `dm/games/` and bring it up as below.
|
||||
2. **Profile.** Copy `010015100B514000/game.tsv` (or the provisional profile) to `dm/games/<title ID>/game.tsv` (the base game's title ID, as the folder name) and fill in the identity, `build` rows and `modules`. Leave the memory values as Mario's until the bring-up measures them ([docs/agents](../../docs/agents/README.md)).
|
||||
3. **Shader tables.** Run `DM_GAME=<id> shader_split.py … dm/games/<id>/shader_map.tsv dm/games/<id>/shader_facts.tsv dm/games/<id>/shader_pairs.tsv`. Add an empty `shader_map.tsv.conflicts.tsv`, `bf_want.tsv` and `pso_manifest.tsv` (copy Mario's headers).
|
||||
3. **Shader tables.** Run `DM_GAME=<id> shader_split.py … dm/games/<id>/shader_map.tsv dm/games/<id>/shader_facts.tsv dm/games/<id>/shader_pairs.tsv dm/games/<id>/pso_pairs.tsv`. Add an empty `shader_map.tsv.conflicts.tsv`, `bf_want.tsv` and `pso_manifest.tsv` (copy Mario's headers).
|
||||
4. **Shaders.** `DM_GAME=<id> [IOSOUT=…] dmsh_build.py <pkg> <cache>`, with a cache directory of the game's own. A full build also writes `c1_full.tsv` and `cb_extent.tsv` (local files, not committed). The chain version needs nothing: it is the same for every game.
|
||||
5. **Host bring-up.** Run `make -C dm/host GAME=<id>`, then work through the bring-up loop in docs/agents. Code that only fits one game is gated by its build ID (`dm_main_build()`), never left to run on every game.
|
||||
6. **IPA.** Use `GAME=<id> dm/ios/tools/build_ipa.sh`, or the NSP script, which now finds the profile.
|
||||
@@ -155,6 +155,8 @@ Resource locations:
|
||||
| `DM_PSO_ARCHIVE=folder` or `off` | Where to keep the per-game pipeline archive of pipeline bases (default: `pso_archive/` beside `pso_user.tsv`), or no archive |
|
||||
| `DM_PSO_MTL4=0` / `=1` | `0`: build render pipelines through the `MTLDevice` as before, with no unspecialized bases and no archive (the Mac's default is on). `1`: turn them on on iOS, where they are off by default |
|
||||
| `DM_PSO_HYBRID=0` | A draw whose pipeline is not built yet waits for it, instead of drawing with the pair's pulling pipeline meanwhile (docs/development.md, "Hybrid pipelines") |
|
||||
| `DM_PSO_PREBUILD=0` or `tool` | No background prebuild of the pulling bases into the archive; `tool`: run the prebuild alone and exit (`DM_PSO_PREBUILD_DEPTH=zero`, `DM_PSO_PREBUILD_THREADS=<n>`) |
|
||||
| `DM_PSO_PAIRS=path` | The prebuild's second pair table (default: `pso_pairs.tsv` beside the shader map) |
|
||||
| `DM_PSO_WAITLOG=1` | Log each draw that waited more than 20 ms for its pipeline, with why no stand-in served it |
|
||||
| `DM_PSO_PULL_ALL=1` | Diagnostic: every draw that has a pulling pipeline draws with it |
|
||||
|
||||
|
||||
+137
-24
@@ -4484,8 +4484,9 @@ static MTLRenderPipelineDescriptor *pso_desc(const pso_key *k, NvShader *vs, NvS
|
||||
// at start, and a base found in one is taken from it directly (MTL4Archive), not through the compiler's lookupArchives, which would capture it again into the next file. A base
|
||||
// taken from an archive costs about 0.06 ms, against 10-80 ms to build one.
|
||||
// DM_PSO_MTL4=0: the MTLDevice path as before (no bases, no archive). DM_PSO_ARCHIVE=off: no archive; DM_PSO_ARCHIVE=<folder>: the archive in that folder.
|
||||
// On by default on the Mac only; on iOS DM_PSO_MTL4=1 turns it on, and with it the hybrid pipelines. The archive's memory is not measured against an iPhone's limit yet, and an
|
||||
// iPhone cannot be given an archive made offline (metal-tt rejects the Metal 4 serializer's pipeline script)
|
||||
// On by default on the Mac only; on iOS DM_PSO_MTL4=1 turns it on, and with it the hybrid pipelines and the pipeline prebuild. The archive's footprint (with the prebuild, about
|
||||
// 850 MB more for Breath of the Wild) is not measured against an iPhone's limit yet, and an iPhone cannot be given an archive made offline (metal-tt rejects the Metal 4 serializer's
|
||||
// pipeline script)
|
||||
static int pso4_on(void) {
|
||||
static int v = -1;
|
||||
if (v < 0) {
|
||||
@@ -4501,17 +4502,20 @@ static int pso4_on(void) {
|
||||
return v;
|
||||
}
|
||||
static id<MTL4Compiler> g_comp4; static id<MTL4PipelineDataSetSerializer> g_ser4;
|
||||
static NSArray<id<MTL4Archive>> *g_arch4; static NSString *g_arch_dir; static int g_arch_seq;
|
||||
static dispatch_queue_t g_arch_q; static _Atomic int g_arch_new, g_arch_sched;
|
||||
// Compiles with the serializer attached hold this shared, a flush holds it alone: a compile that finishes during serializeAsArchiveAndFlushToURL crashed in Metal
|
||||
// (-[_MTLBinaryArchive newArchiverIdWithBinaryKey:...]). Specializations and archive lookups do not take it (they capture nothing)
|
||||
static pthread_rwlock_t g_arch_rw = PTHREAD_RWLOCK_INITIALIZER;
|
||||
// Compaction: every start writes a file or more (each flush captures only what is new; Breath of the Wild 5-7 per start), and each file a base is not in costs a lookup about
|
||||
// 0.06 ms. With more than PSO_ARCH_COMPACT files, the bases the prewarm takes from them are taken through the compiler with those files as its lookup archives, which captures them
|
||||
// again, so the flush after the prewarm writes them into one file; the old files are then removed (open archives stay usable for the rest of the run). Bases the prewarm did not
|
||||
// take (pso_user.tsv never named them) go with them and are built again when drawn
|
||||
// g_arch4: the files that may hold bases with a vertex layout, g_arch4p: the files that hold none (p<seq>.mtl4a: the prebuild's, and any flush whose bases all read their vertices
|
||||
// themselves). A base with a layout is looked up in g_arch4 alone: each file a lookup misses in costs about 0.06 ms, and a cold start's misses were for such bases, in the 13 prebuilt files
|
||||
static NSArray<id<MTL4Archive>> *g_arch4, *g_arch4p; static NSString *g_arch_dir; static int g_arch_seq;
|
||||
// Compaction: every start writes a file or more (each flush captures only what is new), Breath of the Wild 5-7 per start, and each file a base is not in costs a lookup about 0.06 ms.
|
||||
// With more than PSO_ARCH_COMPACT files of bases with a vertex layout, the bases the prewarm takes from them are taken through the compiler with those files as its lookup archives,
|
||||
// which captures them again, so the flush after the prewarm writes them into one file; the old files are then removed (open archives stay usable for the rest of the run). Bases the
|
||||
// prewarm did not take (pso_user.tsv never named them) go with them and are built again when drawn. The prebuild's files (p<seq>) are left as they are
|
||||
enum { PSO_ARCH_COMPACT = 8 };
|
||||
static MTL4CompilerTaskOptions *g_compact_opts; static NSArray<NSString *> *g_compact_files; static _Atomic int g_compacted;
|
||||
static _Atomic int g_arch_new_vd; // bases with a vertex layout captured since the last flush (counted under g_arch_rw's read side, read under its write side)
|
||||
static dispatch_queue_t g_arch_q; static _Atomic int g_arch_new, g_arch_sched;
|
||||
// Compiles with the serializer attached hold this shared, a flush holds it alone: a compile that finishes during serializeAsArchiveAndFlushToURL crashed in Metal
|
||||
// (-[_MTLBinaryArchive newArchiverIdWithBinaryKey:...], the prebuild's first flush). Specializations and archive lookups do not take it (they capture nothing)
|
||||
static pthread_rwlock_t g_arch_rw = PTHREAD_RWLOCK_INITIALIZER;
|
||||
static _Atomic uint64_t g_pb_built, g_pb_archived, g_pb_reused, g_pb_spec, g_pb_legacy, g_pb_base_ns;
|
||||
// Time by kind, all threads (the 'pipelines up to frame' line): archive lookups that found the base / found none, specializations, base compiles; predictions (submit thread)
|
||||
static _Atomic uint64_t g_pt_hit_n, g_pt_hit_ns, g_pt_miss_n, g_pt_miss_ns, g_pt_spec_ns, g_pt_comp_ns, g_pt_pred_n, g_pt_pred_ns, g_pp_done_n; // bases compiled / taken from the archive / reused, specializations, MTLDevice builds
|
||||
@@ -4535,20 +4539,22 @@ static void pso4_init(void) {
|
||||
g_arch_q = dispatch_queue_create("dm.nvn.pso-archive", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0));
|
||||
MTL4PipelineDataSetSerializerDescriptor *sd = [MTL4PipelineDataSetSerializerDescriptor new]; sd.configuration = MTL4PipelineDataSetSerializerConfigurationCaptureBinaries;
|
||||
ser = [g_dev newPipelineDataSetSerializerWithDescriptor:sd];
|
||||
NSMutableArray<id<MTL4Archive>> *arch = [NSMutableArray array]; uint64_t t0 = bp_now(); unsigned long long bytes = 0;
|
||||
NSMutableArray<id<MTL4Archive>> *arch = [NSMutableArray array], *archp = [NSMutableArray array]; uint64_t t0 = bp_now(); unsigned long long bytes = 0;
|
||||
NSMutableArray<NSString *> *runf = [NSMutableArray array];
|
||||
NSArray<NSString *> *names = [[[NSFileManager.defaultManager contentsOfDirectoryAtPath:g_arch_dir error:nil] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self ENDSWITH '.mtl4a'"]]
|
||||
sortedArrayUsingSelector:@selector(compare:)];
|
||||
for (NSString *n in names.reverseObjectEnumerator) { // newest first
|
||||
NSString *p = [g_arch_dir stringByAppendingPathComponent:n]; NSError *ae = nil;
|
||||
id<MTL4Archive> a = [g_dev newArchiveWithURL:[NSURL fileURLWithPath:p] error:&ae];
|
||||
if (a) { [arch addObject:a]; [runf addObject:p]; bytes += [[NSFileManager.defaultManager attributesOfItemAtPath:p error:nil] fileSize]; }
|
||||
int pf = [n hasPrefix:@"p"];
|
||||
if (a) { [pf ? archp : arch addObject:a]; if (!pf) [runf addObject:p]; bytes += [[NSFileManager.defaultManager attributesOfItemAtPath:p error:nil] fileSize]; }
|
||||
else MLOG("[RED] pipeline archive file %s not usable (%s): its bases are built again", p.UTF8String, ae.localizedDescription.UTF8String);
|
||||
g_arch_seq = MAX(g_arch_seq, n.intValue + 1);
|
||||
g_arch_seq = MAX(g_arch_seq, [n substringFromIndex:pf].intValue + 1);
|
||||
}
|
||||
g_arch4 = arch;
|
||||
g_arch4 = arch; g_arch4p = archp;
|
||||
if (runf.count > PSO_ARCH_COMPACT) { g_compact_opts = [MTL4CompilerTaskOptions new]; g_compact_opts.lookupArchives = arch; g_compact_files = runf; }
|
||||
MLOG("pipeline archive %s: %lu files, %.1f MB, opened in %.1f ms", g_arch_dir.UTF8String, (unsigned long)arch.count, bytes / 1048576.0, (bp_now() - t0) / 1e6);
|
||||
MLOG("pipeline archive %s: %lu files (%lu of bases without a vertex layout), %.1f MB, opened in %.1f ms", g_arch_dir.UTF8String, (unsigned long)(arch.count + archp.count),
|
||||
(unsigned long)archp.count, bytes / 1048576.0, (bp_now() - t0) / 1e6);
|
||||
}
|
||||
MTL4CompilerDescriptor *cd = [MTL4CompilerDescriptor new]; cd.pipelineDataSetSerializer = ser;
|
||||
NSError *ce = nil; g_comp4 = [g_dev newCompilerWithDescriptor:cd error:&ce];
|
||||
@@ -4567,7 +4573,8 @@ static void arch_flush(void) {
|
||||
if (!n || !g_ser4 || !g_arch_dir) { pthread_rwlock_unlock(&g_arch_rw); return; }
|
||||
[NSFileManager.defaultManager createDirectoryAtPath:g_arch_dir withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
uint64_t t0 = bp_now(); NSError *e = nil;
|
||||
NSString *path = [g_arch_dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%05d.mtl4a", g_arch_seq++]], *tmp = [path stringByAppendingString:@".tmp"];
|
||||
int vd = atomic_exchange(&g_arch_new_vd, 0); // exact: no compile is between capture and count while the write side is held
|
||||
NSString *path = [g_arch_dir stringByAppendingPathComponent:[NSString stringWithFormat:vd ? @"%05d.mtl4a" : @"p%05d.mtl4a", g_arch_seq++]], *tmp = [path stringByAppendingString:@".tmp"];
|
||||
BOOL ok = [g_ser4 serializeAsArchiveAndFlushToURL:[NSURL fileURLWithPath:tmp] error:&e]; pthread_rwlock_unlock(&g_arch_rw);
|
||||
ok = ok && !rename(tmp.fileSystemRepresentation, path.fileSystemRepresentation);
|
||||
if (!ok) { MLOG("[RED] pipeline archive: writing %s failed%s%s", path.UTF8String, e ? ": " : "", e ? e.localizedDescription.UTF8String : ""); unlink(tmp.fileSystemRepresentation); return; }
|
||||
@@ -4585,7 +4592,7 @@ static void arch_compact_finish(void) { // after the flush that follows the pr
|
||||
if (!g_compact_files || !g_compacted) return;
|
||||
if (!g_arch_flush_ok) { MLOG("[RED] pipeline archive: compaction left the old files (the flush failed)"); return; }
|
||||
for (NSString *f in g_compact_files) unlink(f.fileSystemRepresentation);
|
||||
MLOG("pipeline archive compacted: %lu files gave %d bases to the file just written and were removed", (unsigned long)g_compact_files.count, (int)g_compacted);
|
||||
MLOG("pipeline archive compacted: %lu files of bases with a vertex layout gave %d bases to the file just written and were removed", (unsigned long)g_compact_files.count, (int)g_compacted);
|
||||
g_compact_files = nil;
|
||||
}
|
||||
static void arch_flush_now(void) { if (g_arch_q) dispatch_async(g_arch_q, ^{ arch_flush(); arch_compact_finish(); }); }
|
||||
@@ -4651,22 +4658,25 @@ static id<MTLRenderPipelineState> pso_build_ex(MTLRenderPipelineDescriptor *d, N
|
||||
if (base) g_pb_reused++;
|
||||
else {
|
||||
uint64_t t0 = bp_now();
|
||||
for (id<MTL4Archive> a in g_arch4) if ((base = [a newRenderPipelineStateWithDescriptor:b error:nil])) break;
|
||||
int vd = 0; for (int i = 0; i < 31 && !vd; i++) vd = bk.va[i][0] != 0; // a vertex layout (MTLVertexFormatInvalid is 0)
|
||||
if (!vd) for (id<MTL4Archive> a in g_arch4p) if ((base = [a newRenderPipelineStateWithDescriptor:b error:nil])) break;
|
||||
int from_run = 0;
|
||||
if (!base) for (id<MTL4Archive> a in g_arch4) if ((base = [a newRenderPipelineStateWithDescriptor:b error:nil])) { from_run = 1; break; }
|
||||
uint64_t t1 = bp_now();
|
||||
if (base) { g_pb_archived++; g_pt_hit_n++; g_pt_hit_ns += t1 - t0; }
|
||||
// the compile / flush lock only for what the serializer captures (a recapture or a compile): a compile 0 call that only looked in the archive must not wait for a flush
|
||||
int recap = base && g_compact_opts && atomic_load(&g_warm_running), lk = recap || (!base && compile);
|
||||
int recap = base && from_run && g_compact_opts && atomic_load(&g_warm_running), lk = recap || (!base && compile);
|
||||
if (lk) pthread_rwlock_rdlock(&g_arch_rw);
|
||||
if (recap) { // compaction (see g_compact_opts): taken again through the compiler, which captures it
|
||||
id<MTLRenderPipelineState> cb = [g_comp4 newRenderPipelineStateWithDescriptor:b compilerTaskOptions:g_compact_opts error:nil];
|
||||
if (cb) { base = cb; atomic_fetch_add(&g_arch_new, 1); g_compacted++; }
|
||||
if (cb) { base = cb; atomic_fetch_add(&g_arch_new, 1); g_compacted++; if (vd) g_arch_new_vd++; }
|
||||
}
|
||||
if (!base) {
|
||||
if (g_arch4.count) { g_pt_miss_n++; g_pt_miss_ns += t1 - t0; }
|
||||
if (g_arch4.count + g_arch4p.count) { g_pt_miss_n++; g_pt_miss_ns += t1 - t0; }
|
||||
if (compile) {
|
||||
NSError *be = nil; base = [g_comp4 newRenderPipelineStateWithDescriptor:b compilerTaskOptions:nil error:&be]; job->error = be; job->compiled = 1;
|
||||
g_pt_comp_ns += bp_now() - t1;
|
||||
if (base) { g_pb_built++; if (g_ser4) arch_note_new(); }
|
||||
if (base) { g_pb_built++; if (vd) g_arch_new_vd++; if (g_ser4) arch_note_new(); }
|
||||
}
|
||||
}
|
||||
if (lk) pthread_rwlock_unlock(&g_arch_rw);
|
||||
@@ -4798,7 +4808,7 @@ static int pso_pend_try(const pso_key *k, id<MTLRenderPipelineState> *out) {
|
||||
// pair's pulling pipeline (<key>_bp, dmsh_pull.py: no vertex descriptor; the vertex streams are read through a per-draw layout block, pull_bind), specialized with the draw's colour
|
||||
// state, while the pipeline for its vertex layout is built in the background (pso_pend_demand); a later draw takes that one when it is ready (pso_pend_try). The pulling pipeline costs
|
||||
// vertex time (the run-time format decode: about 1 ms per million vertices on an M2 Max), so it only bridges those frames. Both compute the same positions: [[position, invariant]],
|
||||
// built with preserved invariance, from bit-identical inputs (t_pull.m, t_pull_inv.m). Only a pulling base already made or in the archive is used; otherwise
|
||||
// built with preserved invariance, from bit-identical inputs (t_pull.m, t_pull_inv.m). Only a pulling base already made or in the archive is used (the prebuild makes them); otherwise
|
||||
// the draw waits as before and the pulling base is built in the background for the pair's next miss. Not for merged passes, readable-tile or diagnostic fragments, layer-writing or
|
||||
// transform feedback vertex shaders. DM_PSO_HYBRID=0 turns it off
|
||||
static int hybrid_on(void) {
|
||||
@@ -4848,6 +4858,108 @@ void mtl_pso_line(char *o, size_t n) {
|
||||
memcpy(pn, g_pw_n, sizeof pn); memcpy(pns, g_pw_ns, sizeof pns);
|
||||
}
|
||||
|
||||
// ---- Prebuild: the base of every shader pair of the game, into the pipeline archive ----
|
||||
// For each pair in the profile's shader_pairs.tsv, the base a hybrid draw needs: the pulling vertex entry (<key>_bp; a vertex shader without stage inputs: its own entry), the pixel
|
||||
// shader, no vertex descriptor, one sample, no alpha-to-coverage, the colour state unspecialized: the descriptor pso_desc_pull / pso_desc make at run time, so the archive gives it back
|
||||
// there (the archive matches libraries by content). Bases already in the archive are skipped; what is built goes into the archive, flushed every 1,000 bases (the serializer holds
|
||||
// what it captured until then). Runs once per chain version, depth mode and pair list (pso_archive/prebuilt.tsv): in the app after the prewarm, on a background queue at utility QoS
|
||||
// (DM_PSO_PREBUILD=0: not at all), or as a tool on the Mac (DM_PSO_PREBUILD=tool: build, write, exit). Vertex shaders that write the layer, pairs whose vertex shader has stage
|
||||
// inputs but no pulling library, and integer-output or multisampled bases are left to the draws
|
||||
static _Atomic int g_pre_running; static _Atomic int g_pre_done, g_pre_built, g_pre_archived, g_pre_skipped, g_pre_failed;
|
||||
static MTL4FunctionDescriptor *pre_fn(id<MTLLibrary> lib, NSString *name) { // as shader_load notes them (fn4_note): constants specialized with an empty set
|
||||
id<MTLFunction> f = [lib newFunctionWithName:name]; if (!f) return nil;
|
||||
MTL4LibraryFunctionDescriptor *ld = [MTL4LibraryFunctionDescriptor new]; ld.library = lib; ld.name = name;
|
||||
if (!f.functionConstantsDictionary.count) return ld;
|
||||
MTL4SpecializedFunctionDescriptor *sd = [MTL4SpecializedFunctionDescriptor new]; sd.functionDescriptor = ld; sd.constantValues = [MTLFunctionConstantValues new]; return sd;
|
||||
}
|
||||
static NSDictionary *pre_json(NSString *key) { NSData *d = [NSData dataWithContentsOfFile:[NSString stringWithFormat:@"%@/%@_b.json", g_msl_dir, key]]; return d ? [NSJSONSerialization JSONObjectWithData:d options:0 error:nil] : nil; }
|
||||
static void pre_pair(NSString *vk, NSString *fk, int zero) {
|
||||
@autoreleasepool {
|
||||
NSDictionary *vj = pre_json(vk), *fj = pre_json(fk);
|
||||
if (!vj || !fj || ![vj[@"chain"] isEqual:@DMSH_CHAIN] || ![fj[@"chain"] isEqual:@DMSH_CHAIN]) { g_pre_skipped++; return; }
|
||||
NSString *ventry = zero && vj[@"entry_z"] ? vj[@"entry_z"] : vj[@"entry"], *fentry = fj[@"entry"] ?: @"main0";
|
||||
int pull = [vj[@"pull"] isKindOfClass:[NSDictionary class]];
|
||||
if (!pull && [vj[@"vertex_inputs"] count]) { g_pre_skipped++; return; } // stage inputs and no pulling library: its pipelines need the draw's layout
|
||||
NSError *e = nil;
|
||||
id<MTLLibrary> vl = lib_from_file([NSString stringWithFormat:@"%@/%@%@.metallib", g_msl_dir, vk, pull ? @"_bp" : @"_b"], &e);
|
||||
if (!vl || t_lib_layer) { g_pre_skipped++; return; } // writes the layer: its pipelines take an input primitive class (pso_desc), no pulling pipeline
|
||||
id<MTLLibrary> fl = lib_from_file([NSString stringWithFormat:@"%@/%@_b.metallib", g_msl_dir, fk], &e);
|
||||
MTL4FunctionDescriptor *vf = pre_fn(vl, ventry), *ff = fl ? pre_fn(fl, fentry) : nil;
|
||||
if (!vf || !ff) { g_pre_skipped++; return; }
|
||||
MTL4RenderPipelineDescriptor *m = [MTL4RenderPipelineDescriptor new];
|
||||
m.vertexFunctionDescriptor = vf; m.fragmentFunctionDescriptor = ff; m.vertexDescriptor = [MTLVertexDescriptor vertexDescriptor]; m.rasterSampleCount = 1;
|
||||
for (int i = 0; i < 8; i++) { MTL4RenderPipelineColorAttachmentDescriptor *a = m.colorAttachments[i];
|
||||
a.pixelFormat = MTLPixelFormatUnspecialized; a.writeMask = MTLColorWriteMaskUnspecialized; a.blendingState = MTL4BlendStateUnspecialized;
|
||||
a.sourceRGBBlendFactor = a.destinationRGBBlendFactor = a.sourceAlphaBlendFactor = a.destinationAlphaBlendFactor = MTLBlendFactorUnspecialized;
|
||||
a.rgbBlendOperation = a.alphaBlendOperation = MTLBlendOperationUnspecialized; }
|
||||
for (id<MTL4Archive> a in g_arch4p) if ([a newRenderPipelineStateWithDescriptor:m error:nil]) { g_pre_archived++; return; }
|
||||
for (id<MTL4Archive> a in g_arch4) if ([a newRenderPipelineStateWithDescriptor:m error:nil]) { g_pre_archived++; return; }
|
||||
pthread_rwlock_rdlock(&g_arch_rw); id ps = [g_comp4 newRenderPipelineStateWithDescriptor:m compilerTaskOptions:nil error:&e]; pthread_rwlock_unlock(&g_arch_rw);
|
||||
if (ps) { g_pre_built++; atomic_fetch_add(&g_arch_new, 1); }
|
||||
else { if (g_pre_failed++ < 4) MLOG("[RED] pipeline prebuild: the base of %s + %s failed: %s", vk.UTF8String, fk.UTF8String, e.localizedDescription.UTF8String); }
|
||||
}
|
||||
}
|
||||
// threads: workers; wait: run on the calling thread until done (the tool), else in the background. Returns the number of pairs, -1 when there is nothing to do
|
||||
static int pso_prebuild(int threads, int wait, dispatch_qos_class_t qos) {
|
||||
shmap_load(); pso4_init();
|
||||
if (!g_comp4 || !g_ser4 || !g_arch_dir || atomic_exchange(&g_pre_running, 1)) return -1;
|
||||
// The pairs: shader_pairs.tsv (BNSH variations and the other readers' pairs) and pso_pairs.tsv (agl BAHS program variations, which vs_pad does not read; DM_PSO_PAIRS: another path),
|
||||
// both beside the shader map
|
||||
NSString *pp = [g_shmap_path.stringByDeletingLastPathComponent stringByAppendingPathComponent:@"shader_pairs.tsv"];
|
||||
const char *ev = getenv("DM_PSO_PAIRS");
|
||||
NSString *pp2 = ev && *ev ? @(ev) : [g_shmap_path.stringByDeletingLastPathComponent stringByAppendingPathComponent:@"pso_pairs.tsv"];
|
||||
NSString *txt = [NSString stringWithContentsOfFile:pp encoding:NSUTF8StringEncoding error:nil], *txt2 = [NSString stringWithContentsOfFile:pp2 encoding:NSUTF8StringEncoding error:nil];
|
||||
if (!txt) { MLOG("pipeline prebuild: no %s, nothing to build", pp.UTF8String); g_pre_running = 0; return -1; }
|
||||
if (txt2) txt = [txt stringByAppendingString:txt2];
|
||||
int zero = atomic_load(&g_depth_zero);
|
||||
NSString *stamp = [g_arch_dir stringByAppendingPathComponent:@"prebuilt.tsv"];
|
||||
NSString *want = [NSString stringWithFormat:@"chain\t%s\ndepth\t%s\npairs\t%lu\t%lx\n", DMSH_CHAIN, zero ? "near=0" : "near=-w", (unsigned long)txt.length, (unsigned long)txt.hash];
|
||||
if ([[NSString stringWithContentsOfFile:stamp encoding:NSUTF8StringEncoding error:nil] isEqualToString:want]) { g_pre_running = 0; return -1; }
|
||||
NSMutableArray<NSArray<NSString *> *> *pairs = [NSMutableArray array];
|
||||
NSMutableSet<NSString *> *seen = [NSMutableSet set];
|
||||
for (NSString *l in [txt componentsSeparatedByString:@"\n"]) { if (!l.length || [l hasPrefix:@"#"] || [seen containsObject:l]) continue; [seen addObject:l]; NSArray *c = [l componentsSeparatedByString:@"\t"]; if (c.count >= 2) [pairs addObject:c]; }
|
||||
int n = (int)pairs.count; uint64_t t0 = bp_now();
|
||||
MLOG("pipeline prebuild: %d shader pairs of %s%s%s, %d workers (%s)", n, pp.UTF8String, txt2 ? " and " : "", txt2 ? pp2.UTF8String : "", threads, wait ? "tool" : "background");
|
||||
__block _Atomic int next = 0;
|
||||
dispatch_group_t g = dispatch_group_create();
|
||||
for (int t = 0; t < threads; t++) dispatch_group_async(g, dispatch_get_global_queue(qos, 0), ^{
|
||||
for (;;) {
|
||||
int i = atomic_fetch_add(&next, 1); if (i >= n) break;
|
||||
pre_pair(pairs[(NSUInteger)i][0], pairs[(NSUInteger)i][1], zero);
|
||||
int d = ++g_pre_done;
|
||||
if (d % 1000 == 0) {
|
||||
MLOG("pipeline prebuild: %d of %d pairs, %d built, %d already archived, %d left to the draws, %d failed, %.0f s", d, n, (int)g_pre_built, (int)g_pre_archived, (int)g_pre_skipped, (int)g_pre_failed, (bp_now() - t0) / 1e9);
|
||||
if (atomic_load(&g_arch_new) >= 1000) dispatch_sync(g_arch_q, ^{ arch_flush(); }); // the serializer holds what it captured until written
|
||||
}
|
||||
}
|
||||
});
|
||||
dispatch_block_t done = ^{
|
||||
dispatch_sync(g_arch_q, ^{ arch_flush(); });
|
||||
if (!g_pre_failed) [want writeToFile:stamp atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
||||
MLOG("pipeline prebuild done: %d pairs, %d bases built, %d already archived, %d left to the draws, %d failed, %.1f s", n, (int)g_pre_built, (int)g_pre_archived, (int)g_pre_skipped, (int)g_pre_failed, (bp_now() - t0) / 1e9);
|
||||
g_pre_running = 0;
|
||||
};
|
||||
if (wait) { dispatch_group_wait(g, DISPATCH_TIME_FOREVER); done(); }
|
||||
else dispatch_group_notify(g, dispatch_get_global_queue(qos, 0), done);
|
||||
return n;
|
||||
}
|
||||
// DM_PSO_PREBUILD=tool: the prebuild alone (the host's other environment as for a run: DM_NVN_SHMAP, DM_MSL_DIR, DM_SAVE or DM_PSO_ARCHIVE), then exit; DM_PSO_PREBUILD_DEPTH=zero for
|
||||
// a game that sets the near=0 depth mode, DM_PSO_PREBUILD_THREADS=<n> workers (default: the performance cores less one). Returns 0 when not asked for
|
||||
int mtl_pso_prebuild_tool(void) {
|
||||
const char *e = getenv("DM_PSO_PREBUILD"); if (!e || strcmp(e, "tool")) return 0;
|
||||
setenv("DM_PSO_MANIFEST", "off", 1); // no prewarm (loading the shader map starts it)
|
||||
mtl_init(); const char *z = getenv("DM_PSO_PREBUILD_DEPTH"); if (z && !strcmp(z, "zero")) atomic_store(&g_depth_zero, 1);
|
||||
int threads = getenv("DM_PSO_PREBUILD_THREADS") ? atoi(getenv("DM_PSO_PREBUILD_THREADS")) : 0;
|
||||
if (threads <= 0) { int pc = 0; size_t sz = sizeof pc; if (sysctlbyname("hw.perflevel0.physicalcpu", &pc, &sz, NULL, 0) || pc <= 1) pc = 4; threads = pc - 1; }
|
||||
if (pso_prebuild(threads, 1, QOS_CLASS_USER_INITIATED) < 0) MLOG("pipeline prebuild: nothing to do (no archive, no shader_pairs.tsv, or prebuilt.tsv says it is done)");
|
||||
return 1;
|
||||
}
|
||||
static void pso_prebuild_bg(void) { // after the prewarm, in the app
|
||||
const char *e = getenv("DM_PSO_PREBUILD"); if (e && !strcmp(e, "0")) return;
|
||||
if (!hybrid_on()) return;
|
||||
int pc = 0; size_t sz = sizeof pc; if (sysctlbyname("hw.perflevel0.physicalcpu", &pc, &sz, NULL, 0) || pc <= 1) pc = 4;
|
||||
pso_prebuild(pc > 4 ? pc / 2 : 2, 0, QOS_CLASS_UTILITY);
|
||||
}
|
||||
static __unsafe_unretained id<MTLRenderPipelineState> pso_get_(NvProg *pr, MTLRenderPassDescriptor *unused);
|
||||
static id<MTLFunction> ms_fs_override(NvShader *fs); // DM_NVN_MAGSCAN (defined before draw)
|
||||
static int pf_int_kind(MTLPixelFormat f);
|
||||
@@ -5012,6 +5124,7 @@ static void warm_worker(NSArray<NSString *> *paths) {
|
||||
}
|
||||
atomic_store(&g_warm_running, 0);
|
||||
arch_flush_now(); // flush and finish compaction together, after the prewarm stops recapturing bases
|
||||
@autoreleasepool { pso_prebuild_bg(); } // then, once, the bases every shader pair's hybrid draws need
|
||||
}
|
||||
// Wait for the background warm-up to finish (at most timeout_ms): returns how many milliseconds were actually waited, 0 if it is not running. On first install the system shader cache is fully cold; if warm-up is slower than the player entering a run,
|
||||
// pipelines not yet warmed are compiled on the spot on the submit thread = in-run stutter; the loading screen on entering a run blocks the player anyway, so let it load a little longer (called where hle_nvn.c presents).
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Metal 4 pipelines (pso_build): one base per pipeline with its colour state unspecialized, made into pipelines by specialization, kept in the pipeline archive.
|
||||
// Checks: pipelines that differ only in colour state share one base; a specialized pipeline draws the same pixels as one built through the MTLDevice; a flushed archive
|
||||
// file serves the base again without compiling; a function with no Metal 4 descriptor keeps the MTLDevice path; pso_key_norm drops what no pipeline depends on; compaction
|
||||
// merges the files the prewarm took bases from into one despite a mid-prewarm flush; depth-only draws write depth; concurrent colour variants compile one base
|
||||
// file serves the base again without compiling; a flush of bases without a vertex layout is a p<seq> file, where a base with a layout is not looked up; a function with no
|
||||
// Metal 4 descriptor keeps the MTLDevice path; pso_key_norm drops what no pipeline depends on; compaction merges the files the prewarm took bases from into one despite a
|
||||
// mid-prewarm flush; depth-only draws write depth; concurrent colour variants compile one base
|
||||
#include "../nvn/nvn_mtl.m"
|
||||
uint64_t g_t0_ns;
|
||||
int dm_guest_as_contains(uintptr_t address) { (void)address; return 0; }
|
||||
@@ -134,7 +135,8 @@ int main(void) { @autoreleasepool {
|
||||
mtl_init(); g_q = [g_dev newCommandQueue]; NSError *err = nil;
|
||||
NSString *src = @"#include <metal_stdlib>\nusing namespace metal;\nstruct V { float4 p [[position]]; };\nstruct I { float4 a [[attribute(0)]]; };\n"
|
||||
"vertex V vs(I in [[stage_in]], uint id [[vertex_id]]) { V o; float2 uv = float2((id << 1) & 2, id & 2); o.p = float4(uv * 2 - 1, 0, in.a.w); return o; }\n"
|
||||
"struct O { float4 c0 [[color(0)]]; float4 c1 [[color(1)]]; };\nfragment O fs() { return { float4(0.5, 0.25, 1.0, 0.5), float4(0.2, 0.4, 0.6, 0.8) }; }\n";
|
||||
"struct O { float4 c0 [[color(0)]]; float4 c1 [[color(1)]]; };\nfragment O fs() { return { float4(0.5, 0.25, 1.0, 0.5), float4(0.2, 0.4, 0.6, 0.8) }; }\n"
|
||||
"vertex V vs0(uint id [[vertex_id]]) { V o; float2 uv = float2((id << 1) & 2, id & 2); o.p = float4(uv * 2 - 1, 0, 1); return o; }\n";
|
||||
g_lib = [g_dev newLibraryWithSource:src options:nil error:&err];
|
||||
if (!g_lib) { fprintf(stderr, "%s\n", err.localizedDescription.UTF8String); return 1; }
|
||||
g_lib2 = [g_dev newLibraryWithSource:[src stringByAppendingString:@"// another library: the same source would come back as the same library\n"] options:nil error:&err];
|
||||
@@ -158,10 +160,27 @@ int main(void) { @autoreleasepool {
|
||||
same_pixels("from the archive: RGBA8 blended, RGBA16F", MTLPixelFormatRGBA8Unorm, 1, MTLPixelFormatRGBA16Float, MTLColorWriteMaskAll);
|
||||
EXPECT("base from the archive", 1, g_pb_archived);
|
||||
EXPECT("no base compiled", built, g_pb_built);
|
||||
// a flush whose bases all read their vertices themselves (no vertex layout) is a p<seq> file; a base with a layout is never looked up in one
|
||||
MTLRenderPipelineDescriptor *nd = desc(MTLPixelFormatRGBA8Unorm, 0, MTLPixelFormatRGBA8Unorm, MTLColorWriteMaskAll, 1);
|
||||
nd.vertexFunction = fn4_note([g_lib newFunctionWithName:@"vs0"], g_lib, @"vs0", nil); nd.vertexDescriptor = [MTLVertexDescriptor vertexDescriptor];
|
||||
EXPECT("no layout: a pipeline", 1, pso_build(nd, &err) != nil);
|
||||
EXPECT("no layout: its base compiled", built + 1, g_pb_built);
|
||||
arch_flush();
|
||||
NSString *pfile = [@(dir) stringByAppendingPathComponent:@"p00001.mtl4a"];
|
||||
EXPECT("no layout: written to a p file", 1, [NSFileManager.defaultManager fileExistsAtPath:pfile]);
|
||||
nvn_hm_clear(&g_pbase, hm_rel);
|
||||
id<MTL4Archive> pa = [g_dev newArchiveWithURL:[NSURL fileURLWithPath:pfile] error:&err], va = [g_dev newArchiveWithURL:[NSURL fileURLWithPath:file] error:&err];
|
||||
g_arch4 = @[]; g_arch4p = @[pa, va]; // the layout base's file among the p files: not looked in
|
||||
uint64_t arch0 = g_pb_archived; built = g_pb_built;
|
||||
EXPECT("no layout again: a pipeline", 1, pso_build(nd, &err) != nil);
|
||||
EXPECT("no layout again: base from the p file", arch0 + 1, g_pb_archived);
|
||||
same_pixels("layout base with only p files open", MTLPixelFormatRGBA8Unorm, 1, MTLPixelFormatRGBA16Float, MTLColorWriteMaskAll);
|
||||
EXPECT("layout base: not taken from a p file, compiled", built + 1, g_pb_built);
|
||||
EXPECT("layout base: one waits for a plain file", 1, g_arch_new_vd);
|
||||
// compaction: with more than PSO_ARCH_COMPACT files, the bases the prewarm takes from them are captured again; after the flush that follows, one file holds them and the old ones are gone
|
||||
{
|
||||
char cdir[] = "/tmp/t_pso_compact.XXXXXX"; if (!mkdtemp(cdir)) abort();
|
||||
NSString *saved = g_arch_dir; g_arch_dir = @(cdir); nvn_hm_clear(&g_pbase, hm_rel); g_arch4 = @[];
|
||||
NSString *saved = g_arch_dir; g_arch_dir = @(cdir); nvn_hm_clear(&g_pbase, hm_rel); g_arch4 = @[]; g_arch4p = @[];
|
||||
NSMutableArray<MTLRenderPipelineDescriptor *> *descs = [NSMutableArray array];
|
||||
for (int i = 0; i <= PSO_ARCH_COMPACT; i++) { // one base per file: the stride of the layout tells them apart
|
||||
MTLRenderPipelineDescriptor *cd = desc(MTLPixelFormatRGBA8Unorm, 0, MTLPixelFormatRGBA8Unorm, MTLColorWriteMaskAll, 1);
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
# (SecredUber's 16600 stage shaders have only 3079 distinct control sections, with 6367 cases of same control, different code), so hashing the control alone picks the wrong library.
|
||||
# Output: <package dir>/<stage>_<source group>.bin, each record = 'BTCDBTCD' + header (+0x28 control length, +0x2c code length) padded to 0x38 + code area + control section, one copy per key;
|
||||
# map rows have the same columns as D2R's shader_map.tsv (column 5 records where the key first appeared); the answer table has one row per compute shader {"local", "shared"} (shared memory rounded up to 16 bytes, as D2R did).
|
||||
# Usage: shader_split.py <RomFS dir> <ExeFS main> <output package dir> <output map> <output answer table> [<output pair table>] (decompressing main needs lz4: run with dm's venv python) Environment J=concurrency (default 8)
|
||||
# Usage: shader_split.py <RomFS dir> <ExeFS main> <output package dir> <output map> <output answer table> [<output pair table> [<output pipeline pair table (pso_pairs.tsv)>]] (decompressing main needs lz4: run with dm's venv python) Environment J=concurrency (default 8)
|
||||
# With an update: give RomFS and main as comma-separated lists (base game first, e.g. data/v0/RomFS,data/v327680/RomFS); the output is the union of both versions (see main())
|
||||
# Unity type trees need Unity's common string table, which is taken from the first main
|
||||
# Pair table: (vertex key, pixel key) of every BNSH variation (every Unity program); dmsh_build uses it to give vertex shaders the outputs their pixel shaders read but they do not write (see dmsh_build.py vs_pad)
|
||||
@@ -131,8 +131,46 @@ def bahs_shaders(d, label):
|
||||
st = 'cs' if code[:4] == CS_MAGIC else sph_stage(code) if code[:4] == GFX_MAGIC else None
|
||||
if not st or check(label, st, ctl, code): continue
|
||||
out.append((f'{label}#BAHS@{a:#x}', st, ctl, code))
|
||||
bahs_variations(d, label, [o[0] for o in out])
|
||||
return out
|
||||
|
||||
BAHS_VAR = {} # BAHS entry label → the program variation it belongs to (bahs_variations; var_pairs pairs by it)
|
||||
def bahs_variations(d, label, entries):
|
||||
"""The variations of an agl BAHS file's programs, for var_pairs. The table area holds a binary array (u32 byte size, u32 count, then per binary a record: u32 record size, u32 kind
|
||||
(0 vertex, 1 fragment, 3 compute), two u32, the 16-byte entry bahs_shaders reads) and right after it a program array (u32 byte size, u32 count, then per program: u32 record size,
|
||||
u32 name length, u32 stage mask, u32 index of its first binary, its name, its macros). A program's binaries run from its first index to the next program's; the n-th vertex binary
|
||||
and the n-th fragment binary in that range are one variation. Breath of the Wild: 109 of the 543 pairs drawn on a cold start in Zora's Domain came from these files, and none was in
|
||||
shader_pairs.tsv, which paired only BNSH variations. A file whose arrays do not read back (another layout, or an entry bahs_shaders took that no record holds) gives no pairs"""
|
||||
if not entries: return
|
||||
taken = {int(e.rsplit('@', 1)[1], 16) for e in entries}
|
||||
arr = min(taken) - 0x18
|
||||
if arr < 0x20: return
|
||||
asz, acnt = struct.unpack_from('<II', d, arr)
|
||||
if arr + asz + 8 > len(d) or acnt < len(taken): return
|
||||
o, recs = arr + 8, []
|
||||
for _ in range(acnt):
|
||||
if o + 0x20 > arr + asz: return
|
||||
rs, kind = struct.unpack_from('<II', d, o)
|
||||
if rs < 0x20: return
|
||||
recs.append((o + 0x10, kind)); o += rs
|
||||
if o != arr + asz or not taken <= {a for a, _ in recs}: return
|
||||
psz, pcnt = struct.unpack_from('<II', d, o)
|
||||
if o + psz > len(d) or not pcnt: return
|
||||
q, bases = o + 8, []
|
||||
for _ in range(pcnt):
|
||||
rs, _nl, _mask, base = struct.unpack_from('<IIII', d, q)
|
||||
if rs < 16 or base > acnt or (bases and base < bases[-1]): return
|
||||
bases.append(base); q += rs
|
||||
if q != o + psz: return
|
||||
bases.append(acnt)
|
||||
for p in range(pcnt):
|
||||
seen = [0, 0]
|
||||
for i in range(bases[p], bases[p + 1]):
|
||||
a, k = recs[i]
|
||||
if k > 1: continue # compute and the other kinds pair with nothing
|
||||
if a in taken: BAHS_VAR[f'{label}#BAHS@{a:#x}'] = f'{label}#BAHS#prog{p}#var{seen[k]}'
|
||||
seen[k] += 1
|
||||
|
||||
def mshd_shaders(d, label):
|
||||
out, c = [], d.find(CTL_MAGIC)
|
||||
while c >= 0:
|
||||
@@ -1611,11 +1649,13 @@ def ue_shaders(d, label):
|
||||
return out
|
||||
|
||||
def var_pairs(found):
|
||||
"""(vertex key, pixel key) within the same BNSH variation"""
|
||||
"""(vertex key, pixel key) within the same BNSH variation; (vertex key, pixel key, 'prog') within the same BAHS program variation (bahs_variations), which go to the pipeline
|
||||
pair table only (main: shader_pairs.tsv, which vs_pad reads, keeps the BNSH pairs)"""
|
||||
var = collections.defaultdict(dict)
|
||||
for where, st, ctl, code in found:
|
||||
if '#var' in where: var[where][st] = skey(ctl, code)
|
||||
return {(v['vs'], v['ps']) for v in var.values() if 'vs' in v and 'ps' in v}
|
||||
g = where if '#var' in where else BAHS_VAR.get(where)
|
||||
if g: var[g][st] = skey(ctl, code)
|
||||
return {(v['vs'], v['ps']) + (('prog',) if '#BAHS#prog' in g else ()) for g, v in var.items() if 'vs' in v and 'ps' in v}
|
||||
|
||||
def scan_file(args):
|
||||
"""→ (relative path, {key: …}, pairs, extra); extra = this file's Unity type trees, or 'defer' (no type tree, scanned again in the second pass)"""
|
||||
@@ -1711,6 +1751,7 @@ def nso_image(path):
|
||||
def main():
|
||||
romfs_l, main_l, pkg, mapout, factsout = sys.argv[1:6]
|
||||
pairsout = sys.argv[6] if len(sys.argv) > 6 else None
|
||||
psopairsout = sys.argv[7] if len(sys.argv) > 7 else None # pso_pairs.tsv: the pairs the pipeline prebuild takes besides shader_pairs.tsv (the BAHS program variations)
|
||||
# Several versions (base game + update): the RomFS and main arguments are comma-separated and paired one to one; the output is their union (keys are content hashes, shaders shared by versions appear once).
|
||||
# The first version's source column is unchanged; later versions are prefixed with their workspace directory name (e.g. v327680): /v327680/Shader/…, main@v327680. They sort after the first version, so existing rows keep their package indices.
|
||||
romfs_v, main_v = romfs_l.split(','), main_l.split(',')
|
||||
@@ -1779,6 +1820,7 @@ def main():
|
||||
if not same_shader(keys[key][2:4], v[2:4]): red(f'key {key} is the same but the control section or code area differs ({v[0]} / {keys[key][0]})')
|
||||
continue
|
||||
keys[key] = v; per_src[group(v[0])] += 1
|
||||
prog = {(a, b) for a, b, *t in pairs if t} - pairs; pairs = {x for x in pairs if len(x) == 2}
|
||||
sp, cover = separable_pairs(keys); pairs |= sp
|
||||
if cover: print(f'single-stage programs: {len({v for v, _ in sp}):,} vertex shaders are paired with a cover of {len(cover)} pixel shader(s) ({" ".join(cover)})')
|
||||
os.makedirs(pkg, exist_ok=True)
|
||||
@@ -1820,6 +1862,10 @@ def main():
|
||||
with open(pairsout, 'w') as f:
|
||||
f.write('# kind: evidence vertex / pixel shader pairs within BNSH variations (generated by dm/gfx/tools/shader_split.py, do not edit): vertex key <TAB> pixel key\n')
|
||||
for a, b in sorted(pairs): f.write(f'{a}\t{b}\n')
|
||||
if psopairsout:
|
||||
with open(psopairsout, 'w') as f:
|
||||
f.write('# kind: evidence vertex / pixel shader pairs of agl program variations (BAHS) that shader_pairs.tsv does not hold, for the pipeline prebuild only (generated by dm/gfx/tools/shader_split.py, do not edit): vertex key <TAB> pixel key\n')
|
||||
for a, b in sorted(prog - pairs): f.write(f'{a}\t{b}\n')
|
||||
print(f'shaders: {len(rows)} (' + ', '.join(f'{STAGE_NAME[s]} {n}' for s, n in sorted(stat.items())) + '); sources ' + ', '.join(f'{g} {n}' for g, n in per_src.most_common()) + f'; compute shader answers {len(facts)} rows' + (f'; variation pairs {len(pairs)}' if pairsout else ''))
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
@@ -122,6 +122,7 @@ void dm_host_preload_start(int argc, char **argv) {
|
||||
if (pthread_create(&g_pre.th, &a, preload_thread, NULL)) die("dm_host_preload_start: cannot start the thread");
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
{ int mtl_pso_prebuild_tool(void); if (mtl_pso_prebuild_tool()) return 0; } // DM_PSO_PREBUILD=tool: the pipeline archive's prebuild alone, no game (nvn_mtl.m)
|
||||
if (getenv("DM_PRELOAD")) dm_host_preload_start(argc, argv); // Exercise the early-start path on Mac (on iOS it is called from the App's main())
|
||||
if (atomic_load(&g_pre.started)) {
|
||||
pthread_join(g_pre.th, NULL);
|
||||
|
||||
+7
-1
@@ -226,7 +226,7 @@ The unsigned IPA build still uses the repository's `dm/ios/DmIOS.xcodeproj`, ski
|
||||
|
||||
Each game has a profile in `dm/games/<id>/`:
|
||||
- `game.tsv` holds the title ID, known builds, code modules, RomFS marker, memory split, app name and bundle ID;
|
||||
- the shader tables: `shader_map.tsv`, `shader_facts.tsv`, `shader_pairs.tsv`, `pso_manifest.tsv`, and `c1_full.tsv` / `cb_extent.tsv`, which each checkout's full shader build makes (not committed);
|
||||
- the shader tables: `shader_map.tsv`, `shader_facts.tsv`, `shader_pairs.tsv`, `pso_pairs.tsv` (pairs only the pipeline prebuild reads), `pso_manifest.tsv`, and `c1_full.tsv` / `cb_extent.tsv`, which each checkout's full shader build makes (not committed);
|
||||
- `patches.py` (when there are any): the rewriter's patches of the game's own builds.
|
||||
|
||||
The shader translation chain has one version for every game, in `dm/gfx/nvn/dmsh_chain.h`.
|
||||
@@ -332,6 +332,12 @@ flowchart TD
|
||||
|
||||
Every 300 frames the log has a `pipelines up to frame` line. It gives the submit thread's pipeline, library and prediction time; archive lookups that found the base or none; specializations and base compiles on all threads; draws with a hybrid pipeline; background builds; and the draws that waited, by why no stand-in served them. `DM_PSO_WAITLOG=1` logs every wait over 20 ms with its shader pair. `DM_PSO_PULL_ALL=1` (diagnostic) draws every draw that has a pulling pipeline with it. In steady state the GPU time is the same with and without the hybrid: in Breath of the Wild, mean GPU p50 over frames 2,400–5,100 was 8.98 ms with it, 8.98 ms on the Metal 4 path without it, and 9.35 ms through the `MTLDevice`.
|
||||
|
||||
The **prebuild** puts the pulling base of every known shader pair into the archive (`pso_prebuild`), so a cold start's draws have stand-ins. The pairs come from the profile's `shader_pairs.tsv` and `pso_pairs.tsv`. `shader_pairs.tsv` pairs the stages of one BNSH variation (and the other readers' programs). It held none of the pairs from agl's BAHS shader archives (`.sharcb`, the agl / gsys / terrain resource packs), and in Breath of the Wild 109 of the 543 pairs drawn on entering Zora's Domain came from those. `shader_split.py` now reads the binary and program arrays of a BAHS file (`bahs_variations`) and pairs the n-th vertex binary of a program with its n-th fragment binary. Those pairs go to `pso_pairs.tsv`, which only the prebuild reads. `vs_pad` keeps reading `shader_pairs.tsv`, so the shader libraries, the inputs digest and the other profiles' tables are unchanged. For Breath of the Wild that is 3,012 more pairs, and all 543 pairs drawn are now known. A profile made before the table existed still packs: the NSP script then keeps the table beside the shader package and hands it to the prebuild (`DM_PSO_PAIRS`). Super Mario Odyssey, Mario Bros. Wonder, Mario Kart 8 Deluxe, 3D World, Animal Crossing, Captain Toad and Tears of the Kingdom have BAHS files too; their tables are made at their next pack.
|
||||
|
||||
The prebuild runs in the app after the prewarm, on a utility queue on half the performance cores, flushing every 1,000 bases (`DM_PSO_PREBUILD=0` turns it off). On the Mac it also runs as a tool, `DM_PSO_PREBUILD=tool`: build, write, exit. The NSP script runs the tool in a `--platform mac` build, into `<id>_build/run/pso_archive` (log `pso_prebuild.log`). It runs once per chain version, depth mode and pair list (`pso_archive/prebuilt.tsv`). `DM_PSO_PREBUILD_DEPTH=zero` serves a game that sets the near=0 depth mode; `DM_PSO_PREBUILD_THREADS=<n>` sets the workers. Its files hold bases without a vertex layout only, and are named `pNNNNN.mtl4a`; a base with a vertex layout is never looked up in them, and compaction leaves them alone. Breath of the Wild on an M2 Max: 15,767 bases, 910 MB in 16 files, 187 s with 7 workers for the first 12,755 and 19 s for the 3,012 more. Opening the files took 2.2 s on the first start (0.25 s from the file cache). The host's footprint is about 850 MB larger with them open (2,067 against 1,215 MiB at the title), which is one reason the Metal 4 path stays off on iOS. iOS archives cannot be made offline either: `metal-tt` rejects the pipeline script the Metal 4 serializer writes (format 0.1: "expecting [ instead got {"). On iOS this is a later step.
|
||||
|
||||
Breath of the Wild's cold starts with and without the prebuild and `pso_pairs.tsv`: [its note](../dm/games/01007EF00011E000/README.md#pipelines-on-a-cold-start).
|
||||
|
||||
## Power
|
||||
|
||||
The game's own work and its frame rate stay as they are; the host avoids work of its own that the Switch never did. Every 300 frames the log has a `power` line (CPU energy in watts and per frame, CPU time, wakeups, thermal state, the threads that yield most) and a `residency set` line; [05](agents/05-debugging.md) explains them.
|
||||
|
||||
@@ -676,7 +676,7 @@ def write_profile(pdir, app, f):
|
||||
|
||||
|
||||
# ---- Derived profile (a version the game's profile was not brought up on) ----
|
||||
DERIVED_SKIP = {'game.tsv', 'shader_map.tsv', 'shader_facts.tsv', 'shader_pairs.tsv', 'c1_full.tsv', 'cb_extent.tsv', 'vk_capture.tsv', 'gl_capture.tsv', 'patches.py'} # made again from the packed version's own files (patches.py: the rewriter's patches of the registered builds)
|
||||
DERIVED_SKIP = {'game.tsv', 'shader_map.tsv', 'shader_facts.tsv', 'shader_pairs.tsv', 'pso_pairs.tsv', 'c1_full.tsv', 'cb_extent.tsv', 'vk_capture.tsv', 'gl_capture.tsv', 'patches.py'} # made again from the packed version's own files (patches.py: the rewriter's patches of the registered builds)
|
||||
|
||||
|
||||
def derive_profile(g, pdir, ver, d, mods, app=None):
|
||||
@@ -1294,10 +1294,15 @@ def split_shaders(py, repo, roms, mains, pkg, env, logs, g, provisional=False, o
|
||||
PROG.set(step='building the Mac host for the game\'s Oodle decoder'); env = dict(env, **oodle())
|
||||
PROG.set(step='splitting')
|
||||
tmp = pkg.with_name(pkg.name + '.partial'); rmtree(tmp); (tmp / 'tables').mkdir(parents=True)
|
||||
run([py, repo / 'dm/gfx/tools/shader_split.py', ','.join(map(str, roms)), ','.join(map(str, mains)), tmp / 'pkg'] + [tmp / 'tables' / n for n in names], logs / 'shader_split.log', env=env)
|
||||
run([py, repo / 'dm/gfx/tools/shader_split.py', ','.join(map(str, roms)), ','.join(map(str, mains)), tmp / 'pkg'] + [tmp / 'tables' / n for n in names + ('pso_pairs.tsv',)],
|
||||
logs / 'shader_split.log', env=env)
|
||||
m = re.search(r'shaders: (\d+) \(', (logs / 'shader_split.log').read_text(errors='replace'))
|
||||
if m and m.group(1) == '0': # dmsh_build.py would make no library, and the iOS shader step stop with no word of why
|
||||
raise BuildError('the shader split found no shaders in main or the RomFS: the game keeps them in a form shader_split.py does not read; locate them (docs/agents) and add a reader')
|
||||
# pso_pairs.tsv (the pairs of agl BAHS program variations, which only the pipeline prebuild reads): checked like the others where the profile has one; else kept beside the
|
||||
# package (pso_pairs_path), so a profile made before the table existed still packs and its prebuild still gets the pairs
|
||||
if provisional or g.table('pso_pairs.tsv').is_file(): names += ('pso_pairs.tsv',)
|
||||
else: shutil.copyfile(tmp / 'tables' / 'pso_pairs.tsv', pkg.with_name('pso_pairs.tsv'))
|
||||
if provisional: # a provisional profile takes the tables as they come
|
||||
for n in names: shutil.copyfile(tmp / 'tables' / n, g.table(n))
|
||||
else:
|
||||
@@ -1526,7 +1531,7 @@ def mac_host(py, repo, work, env, logs, g):
|
||||
return host, font
|
||||
|
||||
|
||||
def mac_run(work, game, patched, host, font, msl, aoc, gfx, lang, g, env_file=''):
|
||||
def mac_run(work, game, patched, host, font, msl, aoc, gfx, lang, env, logs, g, env_file=''):
|
||||
"""The command that runs the game in a window with sound after a Mac-only build (PLATFORM 'mac'; docs/development.md, Build by hand (Mac), step 5): its save and temporary storage in
|
||||
<id>_build/run (kept from run to run), the extracted RomFS, the Mac shader libraries (msl), the DLC, the profile's shader map (a host outside dm/host does not find it beside itself;
|
||||
c1_full.tsv and cb_extent.tsv are read beside it), the game language, and the switches of an environment file (DM_ENV_FILE / --env-file)"""
|
||||
@@ -1537,6 +1542,15 @@ def mac_run(work, game, patched, host, font, msl, aoc, gfx, lang, g, env_file=''
|
||||
if gfx == 'nvn': renv['DM_NVN_SHMAP'] = g.table('shader_map.tsv')
|
||||
renv.update(DM_LANG=lang, DM_WINDOW=1, DM_AUDIO=1)
|
||||
if env_file: renv.update(env_file_pairs(env_file))
|
||||
if gfx == 'nvn' and g.table('shader_pairs.tsv').is_file():
|
||||
if not g.table('pso_pairs.tsv').is_file() and (work / 'shader/pso_pairs.tsv').is_file(): renv['DM_PSO_PAIRS'] = work / 'shader/pso_pairs.tsv' # split_shaders' copy
|
||||
# The pipeline prebuild (docs/development.md, Hybrid pipelines): the pulling base of every shader pair into run/pso_archive, so the first run's draws have stand-ins instead
|
||||
# of waiting for the Metal compiler. The host's tool mode skips it when pso_archive/prebuilt.tsv matches (chain version, depth mode, pair list): Breath of the Wild's
|
||||
# 12,755 pairs took about 3 minutes on an M2 Max the first time. It assumes the GL depth mode; a game that sets near=0 builds its bases again in the background in the app
|
||||
PROG.add(1); PROG.stage('Pipeline prebuild', step='building')
|
||||
penv = dict(env, **{k: str(v) for k, v in renv.items()}, DM_PSO_PREBUILD='tool')
|
||||
run([host], logs / 'pso_prebuild.log', env=penv, cwd=rd)
|
||||
PROG.end()
|
||||
q = lambda v: shlex.quote(str(v))
|
||||
return f'cd {q(rd)} && ' + ' '.join(f'{k}={q(v)}' for k, v in renv.items()) + f' {q(host)} {q(patched)}'
|
||||
|
||||
@@ -1819,7 +1833,7 @@ def main(args=None):
|
||||
return
|
||||
if mac: # a Mac-only build ends with the Mac host and the command that runs the game
|
||||
host, font = mac_host(py, repo, work, env, logs, g)
|
||||
cmd = mac_run(work, game, patched, host, font, ios, aoc, gfx, lang, g, args.env_file or DM_ENV_FILE)
|
||||
cmd = mac_run(work, game, patched, host, font, ios, aoc, gfx, lang, env, logs, g, args.env_file or DM_ENV_FILE)
|
||||
rmtree(work / 'tmp')
|
||||
say(f'done: the game runs on the Mac with this command (its save in {work / "run/save"}, kept from run to run; DM_WINDOW=1 opens a window, DM_AUDIO=1 plays sound):')
|
||||
PROG.say(cmd)
|
||||
|
||||
Reference in new issue
Block a user