Repository navigation
Conversation
2 tasks done
stan69b
force-pushed
the
pr/oot-iphone-smaller-textures
branch
3 times, most recently
from
November 12, 2026 12:44
64a2767 to
4d83356
Compare
…k before the commit, so the commit no longer pages the guest's files in from the GPU driver's path, where the M1 iPad froze for good With the guest's memory in files (the iOS app's default when the game's pool is larger than its memory), Ocarina of Time on an M1 iPad (8 GB) froze with sound and no picture, once in Link's house and once at the title movie. A sysdiagnose spindump during the freeze: the encode thread in res_commit <- gmem_map, IOGPUFamily -> the VM -> apfs pagein, blocked by an APFS rwlock owned by a launchd background-I/O thread that stopped 4 ms after it and never ran again. Ordinary page-ins of the same files (700,000 in a run) never hung. file_wire (nvn_mtl.m): a shared buffer whose pages are file-backed (vm_region_recurse_64: external pager) is mlocked before it joins the set, and the mlocks are given back right after the commit, which holds the pages for the GPU from then on. Anonymous memory and Metal's own buffers are untouched, so nothing changes with the guest anonymous (every Mac run unless DM_GUEST_FILE=1). Counted in the 300-frame residency line. t_file_wire checks it (failed before the fix, passes after). iPad, the same route that froze twice: alive past frame 12,900, 20 fps in the house (GPU 34-42 ms), headroom 5.3 GB, commits under 2 ms per 300 frames. Mac (M4 Air 16 GB, idle) with DM_GUEST_FILE=1, a route that stalled at 7-37 s a frame before: played through, frame time p50 16.7 ms, 3.9 GB wired at most. memory.md "The residency commit froze over the guest's files"; Ocarina of Time's note. Rebased onto main 0c07813, whose residency changes are queued and applied by the commit outside g_res_mu (0c07813): res_add_at wires a buffer's file pages before it queues the change, and res_commit takes the wired ranges together with the queue, so each commit gives back after itself exactly the ranges of the buffers it made resident; a range wired for a change queued after it waits for that change's own commit.
…share bytes in a placement heap per 32 MB span, as on NVN: Ocarina of Time no longer runs out of GPU memory going outdoors on an M1 iPad With the render targets' recycled heaps (rh_texture, 7f50222) every live description in a GPU-only pool is a Metal texture of its own. On the M1 iPad (8 GB, recommended working set 5,461 MB) that reached 5,184 MB resident at the outdoor save load (544 heaps of 1,384 MB over 1,455 MB of textures), and every command buffer then failed with kIOGPUCommandBufferCallbackErrorOutOfMemory. window_texture (nvn_mtl.m): a texture whose storage lies in a CPU_NO_ACCESS pool (not sparse, not linear, one sample) is placed in a heap standing for a 32 MB span of guest addresses, at its NVN offset scaled by 9/8 and rounded to Metal's alignment, if its Metal size ends within its scaled NVN range, so textures overlap here where they overlap on NVN. A span's heap is made only once it saves memory; a texture across a span's end tries the half-offset spans; covered bytes are inherited, new ones zero-filled. Placed textures have no GPU-optimized contents: with lossless compression, another format's writes over the same bytes hung the iPad's GPU (kIOGPUCommandBufferCallbackErrorHang by Link's house). Anything that does not fit takes the usual path (image_texture, then rh_texture). On only where recommendedMaxWorkingSetSize is under 8 GB (window_wants): sharing a target pool's bytes corrupted the picture on the Mac in PR seedds#1453 (the game keeps live targets in the part of the full-size scene target its current resolution leaves unused, which only NVN's layout makes safe), so larger devices keep each target's own layout. 8 GB devices have 5.3-5.5 GB of working set, a 16 GB M4 Mac 12,124 MB; the separate heaps needed 5.2 GB at the outdoor save load, sharing 4.3 GB. DM_NVN_WINDOW_ALIAS=0/1 forces it, and a log line says which way it went. The gfx test Makefile runs every test with it off unless the test turns it on, so an 8 GB Mac runs them as a 16 GB one does. Also for reading the set (res_top: the first GPU out-of-memory error, DM_CTL restop, and once when the set passes three quarters of the working set): the set by the source line that added each object, the resident pools by raw table / binding, by who gave an address in them (hle_nvn.c passes the kind: mtl_pool_addressed_by) and by pool flags (mtl_pool_tag), the GPU-made textures by pool, and the spans' counts. Tests: t_window_alias (new; 25 checks with the spans on, direct and deferred encoding, Metal validation; 8 fail with them off, the separate allocations), including the 8 GB rule. t_image_alias, t_rt_recycle, t_alias_takeover, t_upgrade_alias, t_copy_alias, t_rt_lifetime, t_pool_chunks, t_addr_known, t_file_wire pass. memory.md "Textures in GPU-only memory took an allocation per description"; rendering.md's recycled-heap entry links it; env_switches.tsv. Rebased onto main's pool and render-target structure: the virtual pool's runs are tagged after main's one-step mtl_pool_replace (vrun_set); the new pools' tag is set after pool_new_put; the storage range for res_top is kept on the texture before own_alias_publish (nvn_owner.m); a span's heap is an NvImageHeap with window set, which main's family-heap search (own_alias_at_locked) skips and which is freed with its last texture rather than waiting in rh_wait's free list. rendering.md keeps main's Fix and Check wording and adds the window heap. Rebased onto main 0c07813: res_add_at records the adding line in g_res_line beside main's queued change (res_q_put), and sets no g_res_dirty, which the queue replaced.
…edicted sleeps, the waits a sleep ended too late for, how late the sleeps woke and the period: Ocarina of Time makes none, so its slow motion on the M1 iPad is GPU time Before, the power line counted clock waits only for its two top yielders, so a clock wait on any other thread was invisible. On an M1 iPad, Ocarina of Time ran at game speed 0.62 in one run and 0.99 in another with DM_YIELD_CLOCK=0, which pointed at 8bdfcb7's predicted sleep. dm_clock_top (hle_thread.c), printed by the power report every 300 frames (hle_nvn.c) as "clock waits up to frame <n>": the two threads with most clock-wait yields since the last report, their predicted sleeps, how many of those waits had already ended by the first poll after the sleep (slept through), how late the sleeps woke (mean / max) and the learned period. No clock wait, no line. No behaviour changes: counters only. On the M1 iPad, 12 minutes from the title through the Deku Tree (upstream main + this branch, no switches but the device's own): no line at all, so no thread of the game makes a clock wait and the predicted sleep never runs. Its slow motion there is the GPU: views that take 54-67 ms of GPU a frame cannot fit the frame step's longest hold, 3 vsyncs (game speed 0.61, then 0.53), where lighter views hold 20 fps at 34-36 ms. t_clockwait checks the clause (the thread, 40 predicted sleeps, the waits slept through, a 16.68 ms period, and nothing once no clock wait came); t_pollwait, t_preempt, t_condvar, t_condvar_old, t_sync_wait, t_system_event and t_notify_sched pass. threads.md "Clock waits sleep to their predicted end".
…ee's entrance with the guest in files and a 3.2 GB residency set; heavier views are GPU-bound "For an iPhone or iPad build" under Memory: the device (iPad Pro 11-inch 3rd generation, M1, 8 GB, iPadOS 27.0.1), the memory limit and headroom (7.97 GB of room against the 9 GB pool, so the guest in files; footprint 2.2-2.3 GB, headroom 5.87 GB at its lowest; working set 5,461 MB, residency set 3.2 GB with the GPU-only spans on), fps and GPU time (20.0 fps in every window of two 32-minute runs at the Deku Tree's entrance, GPU 34-35 ms, game speed 0.97-1.00; heavier views 54-67 ms of GPU, 11-12 fps and game speed 0.53-0.61; the title and game over screens 61 and 66-74 ms), no clock waits, no switch needed, and what is open: the heavy views' GPU time, a GPU hang on an earlier build in the draws of PR seedds#1842's M1 hangs, one kernel data abort, and PR seedds#1906 for an iOS build on 7ffa63c. Found on: "Textures in GPU-only memory took an allocation per description"; memory.md's two entries link the new section. The Open list's "no iOS run" line points at it.
…height matches the shape the host asked for gives the picture that shape instead of 16:9 A game that renders a wider or taller picture into larger window textures and crops the window to it (Ocarina of Time: a 2536x1152 crop of 2560x1440 textures at 2.2:1) was presented as a 16:9 picture. present_shape / present_fit (nvn_mtl.m) now take the crop's own ratio when it matches the screen shape mtl_present_screen set (screen_aspect any), and keep 16:9 for a crop of any other shape. t_present_crop checks a wider crop, a taller crop, a crop of another shape and a 5:4 crop filling a 5:4 canvas on the classic and Metal 4 presents: 120 / 120.
…e while the game runs, from 0.25 to 6, portrait included game.py: screen_aspect off / on / any (DM_GAME_SCREEN_ASPECT 0 / 1 / 2); on (Breath of the Wild, Animal Crossing: taller than 16:9, set at the start) is unchanged. loader.c (screen_any_apply / screen_any_write / dm_screen_view_changed): for an any profile, DM_SCREEN_ASPECT unset or auto follows the front end's view, 1.777 or 0 is 16:9, a number fixes the shape. It writes the state page: +0xa0 the ratio (0 = 16:9), +0xa4 / +0xa8 the width and height factors, +0xac a change count, +0xc4 / +0xc8 the camera's x / y multipliers (wider than 16:9 keeps the vertical angle, between 1:1 and 16:9 the horizontal one; upright takes the scale of the same screen turned landscape), tells the present the shape, and on a change pushes the display-change notifications (hle_oe.c dm_display_changed) so the game lays out again. front_mac.m reports the window's shape 300 ms after a resize settles; front_ios.m the app window's in landscape (upright stays 16:9 for now). settings.tsv: the Picture shape setting for any profiles (Match window / Match screen, or 16:9), auto by default. Front-ends: new: front_view_aspect() (front.h) answers the game view's width / height and dm_screen_view_changed(aspect) reports a settled resize; a profile with screen_aspect any (Ocarina of Time) follows them under DM_SCREEN_ASPECT auto, and renders 16:9 until a front end provides them (the loader's weak front_view_aspect returns 0).
… for matrices and float pairs in guest memory, and a write watch armed from a probe's register DM_PROBE_FP prints s0-s7 on probe hits. DM_PROBE_FINDMAT=<s>[,<n>] scans the guest heap for perspective matrices and arms the write watch on the n-th at 16:9; DM_PROBE_FINDF=<s>,<a>,<b>[,<gap>[,<n>]] scans the heap and the alias region's mapped parts for two floats and arms it on one. DM_PROBE_WATCH=@xn+<offset>[+<length>] arms the watch on that register plus the offset at the first printed probe hit, for objects whose addresses move between runs (Ocarina of Time's layouts in the alias region); DM_PROBE_XREG=N lets DM_PROBE_X2 dump xN's memory. The watch carries out CAS and single-lane ST1 itself. Found Ocarina of Time's camera copy and HUD screen size.
… its output size, window textures and camera projection follow the shape, with no bars and no stretch screen_aspect any, and four stubs as function-matched others of the frame-step rule (patches.py, found on v131072 main): - output size (main+0x1087f40, the display settings' size setter): the requested size times the shape's factors, capped at 1.5 times the pixels, each side a multiple of 8; the crop, the scene's targets and the dynamic-resolution sizes follow. It copies the camera multipliers the size was made with to state +0xb8 / +0xbc. - window crop (main+0xe9a860, setCrop): a crop beyond the 2560x1440 window textures grows them; the next acquire re-creates them. - camera copy (main+0x33a8c0 +0x2ec): the camera named "Main" hands its cached sead::PerspectiveProjection matrix here each frame; rows x / y times the multipliers, the aspect beside it, in memory and in the registers the frustum code reads after. set(), tanf and the 16/9 literals do not run in play: found with DM_PROBE_FINDMAT and the write watch. - display update (main+0x6502e0 +0x18): flag bit 17 set when the loader's change count moved, so the game recomputes its size and a resized window takes effect. Wider than 16:9 keeps the vertical angle, between 1:1 and 16:9 the horizontal one, upright the scale of the same screen turned landscape. test_oot_aspect.py runs the stubs with unicorn (12 tests). Checked on the Mac at six window shapes resized live (iPhone and iPad upright and landscape, 1700x478, 16:9). The HUD is the next commit.
…instead of a 16:9 layout stretched over the picture Every eui layout file holds a screen size of 3840x2160 (LayoutResData +0x200, file offset 0x2f8). A screen's init (main+0x773b90) builds its orthographic projection from it once and keeps it in the screen's uniform block, which the draw copies to the frame's ring each frame (main+0x2d2ba0). Two stubs, function-matched others of the frame-step rule: - hud upload (main+0x2d2ba0 +0x4c, after the memcpy): the copy's x / y rows times the HUD's multipliers and the size divided by them, each frame, so a resize takes effect at the next one; a capture's block (its own size beside the screen's) is left as it is. - layout size (main+0x583fd0, the getter): the size the HUD draws with for the game's conversions between the layout space and the screen; the screen's init still gets the file's. The HUD's multipliers are the camera's divided by the largest of them and 1: in landscape the HUD keeps the size it has at 16:9 of the same height or width; upright the whole 16:9 space fits the width, smaller, nothing cut off. test_oot_aspect.py: 6 more tests (18). Checked on the Mac at six shapes resized live, the pause menu, the "A Open" prompt and the Kokiri Forest cutscene with its location card. Left open (game note, Screen shape): eui places panes about the screen's centre and does not anchor them to its edges, so the HUD keeps its 16:9 arrangement in the middle (in wide shapes the hearts sit at the 16:9 band's edge, upright the HUD sits around mid-height), and full-screen panes (fades, the menu backdrop, 3840x2160) cover the 16:9 middle only. Moving the edge groups (60x80 nulls at the space's edges) and stretching those panes needs eui's per-frame pane transform, not found yet.
…e screen_aspect key, DM_SCREEN_ASPECT for any profiles, the probe switches, and a reference entry for a camera that caches its projection - Game note: a Screen shape section (the six stubs, the rule for wider, taller and upright, the HUD, what is open: the HUD's edge groups and full-screen panes, upright on iOS, the vertical angle upright) and its line under Found on this game. - docs/reference/rendering.md: a screen-shape stub on the projection setter changes nothing, the camera caches its matrix (find the per-frame reader with DM_PROBE_FINDMAT, DM_PROBE_FINDF and the watch armed from a register); the symptom index regenerated. - dm/games/README.md: the screen_aspect key (off / on / any). docs/env_switches.tsv: DM_SCREEN_ASPECT's any range and auto, DM_PROBE_FINDMAT, DM_PROBE_FINDF, DM_PROBE_XREG and DM_PROBE_WATCH's heap+ and @xn forms. docs/settings.md: Picture shape for any profiles. - docs/agents/game-patches.md regenerated: Ocarina of Time's screen-shape and HUD stubs are function-matched (BINDING rows in its patches.py), and Animal Crossing's row names them as the model. docs_patches.py's dry run puts a rule's other functions 4 MB apart instead of 16 MB: with 16 MB the ninth (Ocarina's layout size) landed out of a branch's reach of the patch area.
…ecord is unwired at once instead of the list being lost and written through a null pointer clang-tidy (bugprone-suspicious-realloc-usage) on 29fa5b0's g_fwire = realloc(g_fwire, ...). t_file_wire: 10 / 10.
…the GPU never reads shader code, so nothing makes those pools resident and wires their pages for the table's sake (Ocarina of Time on the iPhone: 223 pools, 317 MB) nvnMemoryPoolGetBufferAddress on a pool with NVN_MEMORY_POOL_FLAGS_SHADER_CODE (not virtual) reports kind 4, which nvn_mtl.m records for res_top but leaves out of the addressed ranges. A buffer made in such a pool still puts it in (nvnBufferInitialize reports kind 0 and 1). On the 8 GB iPhone the wired pools left too little room for the guest's file pages, which were read back 0.2-0.4 GB every 10 s while playing. t_addr_known checks it.
… that measured the same work, so a loading screen's GPU time no longer starts probes of 60 fps nvn_fstep_gpu_slowed compares the GPU time at a longer hold with a running mean of the frames at kmin; 1.3 times as much is taken for a clock that fell with the hold and starts a probe back to kmin. In Ocarina of Time on an M4 Air the only long run at hold 1 was the title and the loading screen (GPU 0.2 vsyncs, 2-60 draws). Gameplay (700-900 draws, 1.3 vsyncs at hold 1, 1.4 at hold 2) read as a GPU slowed 6 times: 32 probes of hold 1 in a 120 s walk, all failed, each failure's late frames stepping on to hold 3. nvn_fgpu keeps the draws a frame encodes beside each reference (dref_at) and at the longer hold (dcur); fstep_tick takes them from mtl_stats. The GPU counts as slowed only while those are within FSTEP_GPU_WORK (1.5) of each other; without draw counts (0) it is as before. t_fstep (81 checks, 0 failed): a loading-screen reference (30 draws) against gameplay at 900 is not slowed and makes no probe, where the same run without draw counts makes 8 failed probes; the same draws at both holds still give Breath of the Wild's one probe that holds. Measured on the M4 Air, Link's house (120 s walk): probes 32 (all failed) -> 7 (all at the title and the load), frame max 122 -> 73 ms, p95 51.1 -> 51.8 ms, but 27.2 -> 24.2 fps: the failed probes' late frames had stepped down to hold 2 now and then, and the hold-3 stretches left come from GPU time at hold 2 of p90 33 ms against 33.3. Kokiri Forest: 19.0 fps, every frame at hold 3, GPU span 42.4 ms, game speed 0.95; with DM_FPS_TARGET=30 21.0 fps at hold 2, game speed 0.92-0.94. Not run: regress.py (the owner's rule for this branch); BotW and TotK, which use the same controller, were not measured.
… where Metal's working set is under 6 GB On a small device (the iPhone 17 and the M1 iPad, working set 5,461 MB), a texture made from the game's data (pkg_upload_) is uploaded half size with one level fewer: Metal level k holds the game's level k+1. This applies to 2D or 2D array textures with both sides 512 or more and 2 levels or more, and to nothing that renders into or writes the texture (image, compressible, display, video, linear, sparse, depth, CPU-repacked formats). The GPU's direct deswizzle writes every level, so a dropped texture takes the staged path. Only sampling sees the smaller texture. tex_mtl hands it out to the sampled-texture path alone (treg_tex sets t_drop_ok), and a sampled view's levels shift by one (drop_levels). Every other use (attachments, copies, image slots in img_binding, readbacks, upgrades) makes the texture whole again first (tex_undrop), and from then on it stays whole (g_drop_full, keyed by its storage). tex_upgrade carries the mark to a format view of a sampled texture. Shaders that fetch texels, ask the size or pick an explicit level read the smaller texture. The memory report counts dropped textures and MB saved, textures made whole by caller line, and candidates by their largest side (256 / 512 / 1024 / 2048+), so a run shows what each threshold would save. DM_NVN_TEX_DROP_MIP=0/1 forces it off or on. Test: t_tex_drop_mip (177 checks: the device rule, level shift by the CPU and the GPU paths, a 2D array, a re-upload, whole again after another use, kept textures, view levels). t_direct_upload, t_view_levels, t_pfv, t_upgrade_alias, t_upload_slots and check_t_window_alias pass. test_env_switches already fails on iphone-a for other switches (DM_NVN_HEAP_RELAXED, DM_NVN_SUB_DRAIN, removed ones); the new row is not among them. Rebased onto main's partial re-upload (pkg_written, up_copy_add) and its reup_note: a texture that leaves out its largest level is re-uploaded whole, as before that change, since the written images and the copies from the texture it replaces count Metal levels from the game's level 0. tex_undrop calls main's pkg_upload with its eight arguments, marks the package read whole (part, lost_gen) and notes its serial with own_note, main's name for ser_note.
…ure whole, and the memory report counts why textures stay whole On the iPhone 17, smaller textures dropped none in Ocarina of Time: 1,003 of the uploads of 256 and more were refused for write usage, because the game gives its material textures NVN_TEXTURE_FLAGS_IMAGE (img_usage then adds ShaderWrite); 165 more had one level, and 2 were of another type. A shader writes such a texture only through an image slot, and img_binding already makes a dropped texture whole first (tex_undrop), as an attachment does. So only render-target usage and the compressible, display and video flags keep a texture whole now. The memory report gets a line of the textures kept whole, by the first rule they fail (type, one level, samples / linear / repack, sparse pool, render-target usage, flags with their bits, depth, made whole before). Test: t_tex_drop_mip 184 checks. An image-flagged texture is dropped when sampled, and made whole with its write usage for another use.
…e iPhone 17's A/B (page-ins 468,772 → 32,707, 24.0 → 29.3 fps at the 30 fps cap), the symptom index and a line in Ocarina of Time's note The reference entry "Large sampled textures leave out their largest level on small devices", which the code comment above drop_wants already names: the symptom (the guest paged back from disk through play), the cause (each texture made from the game's data held twice, as guest bytes and as a Metal texture), the rule and what keeps a texture whole, and the result. A/B on the iPhone 17: prmem_off (DM_NVN_TEX_DROP_MIP=0) against prmem_on (the default), portrait, 30 fps cap, dynamic resolution on, frames 6,000-22,500, scripted by the work folder's m-run/iphone_cam.sh. 672 textures dropped, 396 MB saved, none made whole again; 24.0 → 29.3 fps, late frames 110 → 12 per 300, windows over 60 ms 47 → 3 of 56, page-ins 468,772 → 32,707, resident 3,321 → 2,769 MB, headroom 2,822 → 3,374 MB, no visible blur. 8 GB Macs may also fall under the 6 GB rule; the entry says so.
…r and agl_ndsr_vr_light packs count as the game's upscaler, and their square-root-encoded input is squared before MetalFX scales it in HDR mode The launcher's Game's upscaler MetalFX never applied to Ocarina of Time: UPSCALER_SOURCE (dm/games/game.py) matched only agl_ndsr.pck, and the game's network comes from agl_ndsr_vr_light.pck. It now takes agl_ndsr(_vr)?(_light)?.pck (59 keys for this game). With the keys named, every variant was refused as "neither of the colour shapes MetalFX draws": those networks read the square root of the colour (a pre-pass packs it as RGB9E5), add their residual in that space and decode with a signed square, x * |x| (HMUL2 R, R, |R|), unclamped. nvn_mtl.m now reads that shape from the code (up_mfx_squares, UP_COLOR_SQRT), takes the pack's own rectangles from its push constants (up_mfx_rect_vr), draws the input squared into the scaler's RGBA16F texture (up_mfx_square) and runs MetalFX in HDR mode; the result goes straight into the game's image when it is a whole private RGBA16F one. The variants that encode their output to sRGB, the pre-pass and its sibling keep their shader. agl_ndsr_vr.pck (single-precision squares, Bananza's rectangles) is not taken. Measured on the iPhone 17 (run b2_play1, hand play through the Great Deku Tree, 30 fps cap, dynamic resolution on): the 5 networks met were drawn by MetalFX, the 3 others kept their shader, scalers from 943x434 up to 1508x695 → 2120x976, and the 30 fps cap held while dynamic resolution rose. The owner saw no wrong picture, "maybe deeper colours". No A/B of the frame time yet. Test: check_t_upscaler_mfx passes under Ocarina of Time, Donkey Kong Bananza and Splatoon Raiders (110 checks with MetalFX, encoding thread off and on; 24 with the game's shader): signed squares, the variable-ratio rectangles, a square-root input whole and as a subrectangle above 1.0. test_game_profile, test_settings and test_docs pass. Docs: rendering.md's two upscaler entries, settings.md's Game's upscaler, a line in the game note.
… is under 6 GB, the rule of smaller textures: 125 MB less resident for Ocarina of Time on the iPhone 17 at the same speed Each of the three upload slots keeps its staging after a batch completes (a primary block grown to the slot's demand and one overflow, up to 192 MB), so that a scene's load does not allocate and retire blocks every frame. In play a game uploads a few MB per 300 frames, and on a small device the blocks stay resident for nothing. up_reuse now defaults to the original one-block policy where small_working_set holds (the helper drop_wants uses: the iPhone 17, the M1 iPad, 8 GB Macs), with a log line at the first upload; the Mac's default is unchanged. DM_NVN_UPLOAD_REUSE=0 / 1 still forces either way. Measured on the iPhone 17 (r1_base against r1_reuse0, DM_NVN_UPLOAD_REUSE=0, landscape, 30 fps cap, dynamic resolution on, frames 6,000-22,500, smaller textures on): resident 2,709 → 2,584 MB, footprint 2.63 → 2.51 GB, headroom +125 MB, speed unchanged (29.1 against 29.2 fps, frame p50 34.0 ms both, 19 against 18 late frames per 300). That build had the reuse off on every device (iphone-exp 239f3ae1); this commit gates it by the working-set rule instead. Open: releasing only once uploads have gone quiet, which would keep reuse for a small device's loads. Test: t_upload_slots checks the rule (5,461 MB releases; 6 GB, 12,124 MB or none keeps) and now runs with reuse forced on and off, so it no longer depends on the Mac's memory. t_upload_slots (also with DM_NVN_TEX_DROP_MIP=1), t_upload_budget, t_tex_drop_mip, t_direct_upload, t_pool_standin, t_view_levels, t_pfv, t_upgrade_alias and t_upscaler_mfx pass; test_env_switches and test_docs pass. Docs: a memory.md entry, the switch's row, the comment above UP_BLOCK_SIZE, a game-note line.
…e of the game's pixels, 0.5 to 1.5, opt-in: an iPhone 17 upright renders 976x2120 at 1 instead of 1200x2600 For a screen_aspect any profile, the loader scales both size factors (state +0xa4 / +0xa8) by one number so their product is the bound (loader.c screen_pixels_cap, read once); the profile's stub keeps the shape, the multiple of 8 and the camera, under its own 1.5x bound. Unset or 1.5 changes nothing. Upright on the iPhone 17 (0.460) 1200x2600 → 976x2120 at 1; in its landscape (2.17) 2120x976 instead of about 2352x1080. A value outside 0.5-1.5 gives a red line and keeps the profile's bound. Ported from iphone-exp 79718a29 with one fix: its "picture held" line sat between the shape line's if and its else, so every uncapped shape not held by DM_SCREEN_PIXELS logged "screen shape 16:9" after its own line again (the bug 6682806 fixed); the line is now inside the shape's branch. The owner played the iPhone 17 in landscape with DM_SCREEN_PIXELS=1 (runs b2_play1, b2_play2): "very smoothly, practically locked 30 fps". No A/B against 1.5, so the GPU saving (about 1-1.5 ms a frame) is an estimate, not a measurement; the default stays as it is everywhere. Test: test_oot_aspect (the phone upright, and lower bounds 0.5 / 1 / 1.25 over six shapes and three requested sizes: down to the bound only, the same shape, multiples of 8), test_env_switches and test_docs pass; the host builds. Docs: the switch's row, the game note's Screen shape rule.
…ree to the Kokiri Emerald at the 30 fps cap, with the eight runs behind it, what did not help, frame generation and what is open A dated entry under "For an iPhone or iPad build": the build (smaller textures, staging released, MetalFX for the upscaler, DM_SCREEN_PIXELS=1; branch iphone-exp, chain 39eb2f781d07, PR seedds#2241's frame generation for part of the play), a table of r1_base, r1_reuse0, r1_chunk256, r1_nocomp0, r2_mfx_game, r2_mfx_on, b2_play1 and b2_play2 over frames 6,000-22,500, made from the logs. r2_mfx_on is marked as what it was: MetalFX refused every variant before the square-root shape. Not this: DM_GUEST_FILE_CHUNK=256M (noise) and DM_NVN_FAMILY_NOCOMPRESS=0 (no gain; the rule protects the M1 iPad from a GPU hang). Frame generation: 9,000 frames generated with no restart, the boss at 29.3-29.9 fps shown at 60. Open: the movie cutscenes at 15-18 fps (nn::ndve's network 35-70 ms of GPU), the freeze at a scene state's first visit, the empty CPU profile (seedds#2197) and DM_MEMSTAT on iOS.
…a buffer outside the residency set (t_res_absent, DM_RES_PROBE=1) The raw pool table makes every pool it lists resident, because a shader reaches pools through pointers loaded from memory, which Metal cannot see. Whether a pool could stay out of the set until a shader needs it depends on what such an access does. nvn_res_probe.inc measures it: a Metal 4 queue with the residency set on the queue, as the host has it; a no-copy shared buffer (anonymous memory or a shared file mapping, as the game's memory is on iOS) read or written through a pointer held in a resident argument buffer; never resident, evicted (at once, after 32 commits, after 2 s), another buffer over the same pages resident, made resident late, against resident controls. Apple M4, macOS 27.2: no case faults the command buffer. A read of a buffer that was never resident returns zeros, and a write is dropped. A buffer removed from the set still reads correctly after 32 commits but no longer after 2 s. A buffer added after a zero read reads correctly from the next commit. A second buffer over the same pages does not help (each buffer has its own GPU address). dm/gfx/tests/t_res_absent runs it on the Mac. The host runs it once at start with DM_RES_PROBE=1 (mtl_init, its own queue and set, "[res-probe]" lines) to measure the iPhone. The probe keeps its feedback dispatch queue for good, as the host does: freed with the probe's queue, it was released once too often (SIGTRAP in _os_object_release).
…sidency set returns zeros, measured on the iPhone 17 as on an M4 Mac memory.md gets a new entry: no command buffer error in any case. A buffer never in the set reads 0 and drops writes (anonymous memory, a shared file mapping, file pages only in the cache). Another buffer over the same pages does not help. A removed buffer still reads after 32 commits and reads 0 after 2 s, and a buffer added late reads from the next commit. So residency on demand learnt from a shader's log would draw the first frame that reaches a pool from zeros and lose its stores, with nothing to tell. The pools stay resident: no load log, no chain change. A line in Ocarina of Time's note, and the symptom index.
…extures at one storage address mem_census (DM_CTL restop) summed each texture's allocatedSize over the textures at one storage address. Textures placed in one heap share its bytes, so a resolution family of 36 sizes in one 15 MB heap counted as 490 MB. On Ocarina of Time at the field's edge (16 GB Mac, 2560x1440) the line said 1,131 MB of duplicates, 1,089 MB of them family members; counted by allocation, a heap once, the same route reads 78-122 MB. The line now gives both figures. A new line gives who holds those textures (live serial, finalized serial, pin, sharing table, live or retired descriptor), whether they are resident and where their memory is (own recycled heap, family heap, span heap), and one gives their sharing-table entries by submissions since a lookup last found them. In Ocarina of Time every such texture is held by a live texture object of the game's: the game keeps up to 67 sizes of one shadow target at one address and never finalizes them, so alias_take_over keeps them as it should. 64 MB of the remaining 85 MB is one 4096x4096 target seen as Depth32Float and R32Float. Gathering the copies made outside a family's heap into it (tried: their serials and views dropped when a fresh family heap was made) changed nothing measurable on the same route (85-86 MB after, 78-82 before) and loses a copy's own bytes, so it is not part of this. Rebased onto main's resource owner (nvn_owner.m): the census asks it who holds a texture (own_shared_locked, and two new accessors, own_ghost_holds_locked and own_alias_seen_locked) and walks the sharing table with own_alias_each_locked instead of reading its tables.
…ory line gives the system's free and wired memory An iPhone 17 (8 GB, iOS 26.6.1) ended Ocarina of Time for proc-thrashing at the entry to Hyrule Field with 67 MB free and 5.56 GB wired, while os_proc_available_memory, the only figure the log had, still read 2.7 GB. - A DISPATCH_SOURCE_TYPE_MEMORYPRESSURE source logs each event (warn, critical, back to normal) with the system's free, wired, compressed and file-backed memory, the residency set and the headroom, and asks the submit thread for a trim. At the next submission's start (mem_trim), before anything is encoded and with nothing waiting on the GPU, it frees every recycled render-target heap waiting for a target (rh_trim_n, which rh_trim now uses), the staging blocks of upload slots with no batch open or in flight, and drains the deferred releases whose submissions completed; it logs what it freed. iOS trims at warn and critical, the Mac at critical only; DM_NVN_MEM_PRESSURE=0 logs only, 1 / 2 set the level. - A "system memory at frame" line beside the memory line (host_statistics64: free, wired, compressed, file-backed, purgeable, the residency set and the pressure counts). The memory line now also prints when the system's wired memory moved by 256 MB or a pressure event came. On a 16 GB Mac the source saw a warn walking out of the field's tunnel (free 104 MB, wired 7.9 GB). t_rt_recycle checks that a warn leaves the Mac alone and a critical frees every waiting heap, even a fresh one, takes it out of the residency set, and gives back the idle slots' blocks.
…t, and the residency line gives the load's peaks up_big_src makes a zero-copy buffer over guest pages for each one-time texture read of a large pool or an unaddressed one, held in the set by its upload batch until the batch completes. The batch's 128 MB staging budget did not count them, so one batch could hold any amount: on the iPhone 17, with the guest in files, the field's load added 732 MB of them within 300 frames and 1,022 MB of file pages were wired before their commits. Past DM_NVN_UPLOAD_SRC_MB (96 MB) the batch is now flushed at the next texture's start (the place up_alloc already splits it), and up_begin waits for one of three slots, so about three budgets are wired at most. file_wire, the M1 iPad's APFS hang guard, is unchanged. The residency line adds the set's largest size after a commit, the most file-backed bytes wired before one commit, and the most bytes of these reads in the set at once. On a 16 GB Mac (the field route, guest in files) the budget does not bind: the reads peaked at 233 MB at once without it and 226 MB with it; the set's peak at the load was 4,316 / 4,312 MB, and up to 874 / 867 MB of file pages were wired before one commit, all from memory pools made resident (gmem_map), not these reads. Whether it binds on the phone's slower GPU is for the phone run to tell. Rebased onto main 0c07813 and 0c23e0b. With main's queued residency changes, res_commit records the most file-backed bytes wired before one commit when it takes the wired ranges with the queue, and the largest set after a commit under g_res_commit_mu, where main reads the set; the residency line resets that peak from main's snapshot of the set. The first source buffer may flush and begin a batch (its budget), so the batch that holds it is read after up_big_src; the second one is made, without splitting, only when up_alloc flushed that batch, as main's 0c23e0b does. t_upload_src (main's 0c23e0b) turns this budget off (DM_NVN_UPLOAD_SRC_MB=0): with it, each of its 64 MB textures starts a batch of its own, so only staging should split its batches there; t_upload_budget tests the budget.
…load reads' budget, the census by allocation, and the iPhone 17's death entering Hyrule Field
…ule Field to the castle town, which thrashes under 10 fps
…e 17 thrashed at under 1 fps with 6.19 GB wired: the death is not fixed
…rashing 11 minutes in, back on the field after the castle town
…re event and once past three quarters of the working set after any commit, and its line gives what each source line holds The iPhone 17 is ended for proc-thrashing, with no GPU error, so no log of it had a residency breakdown: the one-time res_top at three quarters of Metal's working set (4,096 MB) was checked only at the 300-frame line, which read 4,059 MB at most while the set passed 4.4 GB between lines. - res_commit checks the three-quarters line after each commit. - A memory pressure warn or critical runs res_top and mem_census, each level at most once a minute. Both reports run on a utility queue (res_report): res_commit is called with g_mu held on some paths, and the census takes it. - The residency line adds what is in the set now by the line that added it (adds less removals, the eight largest) and each one's change since the last line, so a load's leftovers show. - The 300-frame line's buffer for it grows from 400 to 1,400 bytes: the upload reads' figure was cut off on the phone. Mac reproduction (16 GB M4, guest in files, DM_NVN_WINDOW_ALIAS=1 DM_NVN_TEX_DROP_MIP=1, the field save, then four tunnel / Kokiri Forest round trips): the set was 3.46 GB on the field, 4.23 GB after the first load into the forest (the 12288x12288 Depth16 target's own 296 MB heap, +149 MB of textures uploaded from pools, other targets outside the spans), then 4.26-4.28 GB in the forest and 4.37-4.39 GB in the tunnel, 5-15 MB more per round trip. The set grows on each new area's first load and does not grow with going back and forth. Rebased onto main 0c07813: res_add_at and res_remove count each source line's bytes (rs_live_add) beside main's queued changes and set no g_res_dirty; res_commit takes the set's size after its commit under g_res_commit_mu and checks the three-quarters line after releasing it (res_report takes no lock).
…ency set and whether its pool lives, and textures over a buffer count nothing in the set's breakdowns - res_bytes: a texture made over a buffer (tex_linear's linear textures over their pool's buffer) is that buffer's memory. MTLResidencySet counts it once (measured: an 8 MB buffer and 31 textures over it read 8 MB in allocatedSize), but res_top, the additions by line and the set by line summed each texture's allocatedSize, the whole buffer. Ocarina of Time keeps 31-64 linear targets over one 8 MB pool (flags 0xa2): the iPhone 17's "line 4926: 64, 512 MB" back on the field was memory the set did not hold. The set's own total was always right. - mem_census adds the set's textures by the line that added them, by holder (live serial, finalized serial, pin, sharing table, live or retired descriptor) and by whether the memory pool of their storage still exists. Uploaded textures now keep their storage range for it (g_tex_up_key, apart from res_top's GPU-made textures). What accumulates on the Mac reproduction (field, Kokiri Forest, back in the tunnel): textures uploaded from guest memory (the upload's res_add), 1,176 / 1,713 / 1,935 of them, 467 / 634 / 829 MB, all named by live descriptors of texture objects the game has not finalized, in pools that still exist. They are the game's own live textures; on NVN they cost nothing beyond their pools, here each is a Metal copy. Nothing in the host holds one after the game lets it go: 17-23 textures (10-12 MB) are named by nothing. Freeing the copies would be an eviction of host copies of live textures (serial table, packaged entry, sharing table and pool tables at once), which is not done here. Rebased onto main's resource owner (nvn_owner.m): the census asks it who holds a texture (own_ghost_holds_locked, own_shared_locked) instead of reading its tables. Rebased onto main 0c07813: res_add_at and res_remove count res_bytes beside main's queued changes, without g_res_dirty.
…e Mac, the breakdowns' count of textures over a buffer, and the reports at memory pressure The set grows on each area's first load (3.46 GB on the field, 4.23 GB in Kokiri Forest) and not with going back and forth (5-15 MB per round trip over four). What grows is Metal copies of textures the game keeps live in live pools (467 / 634 / 829 MB); freeing them would be an eviction of host copies, not done. The raw-table-only pools (710-778 MB) stay: no safe rule leaves them out.
…d through the castle town and Lon Lon Ranch at 6.27 GB wired; guest pools hold 2.1 GB of its set
…thin 3 s, or any spare at memory pressure, leaves the residency set The uploaded textures the census found held by nothing (line 3664 [ P]; on the iPhone 17 up to 168, 123 MB) were not left over from a texture ID registered again: an ID's views never own residency (defer_obj_unref), and every base texture is owned by the serial table, directly or through its NvPkg. The census looked up the NvPkg, not its textures. With letters for them (k / K a live / only a finalized serial's packaged texture, e a spare), the Mac route shows every uploaded texture as [kv P] and no re-uploads on pool content change; on the phone the unheld ones went from 40 (6 MB) to 168 (123 MB) as those re-uploads went from 0 to 129. Each re-upload keeps the texture it replaced as the package's spare, resident until the texture object is finalized, which pays only when the CPU rewrites the texture again soon (a movie frame). spare_trim: at a submission's start (at most every 250 ms) a spare older than 3 s is retired through defer_obj_release, as a spare a re-upload cannot take always was: after the submissions that used it, and only once no serial entry holds it. mem_trim retires every spare and logs them. The report gives the spares held and retired. t_pkg_spare: an idle spare and one at a critical trim leave the set while the current texture stays and resolves, the next re-upload makes a new texture, and a re-upload within the limit still reuses the spare. Rebased onto main: the spare's note sits beside main's own_note (nvn_owner.m), and the census finds the packages' textures and spares with own_each_locked.
…der reaches: 831 of Ocarina of Time's 851 such shaders take an address through a loaded pointer or a loop, and every frame runs about 185 of their draws Measured, nothing changed in code: the pools the raw pool table lists stay resident. The address of each raw access in the 851 translated shaders that take the table was traced back through their MSL (598 read it through a raw pointer, 233 more through a loop or branch variable, 20 only from buffer words at fixed places); the Mac's field route binds the table about 185 times a frame in every window (raw pool table words at commit).
…tures nothing used for 30 s are freed and made again at their next read On NVN a texture costs nothing beyond its pool; here each uploaded one is a Metal copy, and the game keeps the texture objects of areas it has left (0.6-1.0 GB of the iPhone 17's residency set). mem_trim now frees the packages (NvPkg) no texture ID naming their serials was used by for 30 s, that nothing resolved either, that are not sparse, placed or bound as a view, whose storage the GPU never wrote (gws_owned) and that no pin, sharing entry or takeover holds. Everything naming the copy lets go together: the serials' entries, live and finalized, the storage's sharing entry, the IDs' views, the view cache, and the pool tables, which take the IDs again as a copy at their next use; the texture goes through deferred release after the submissions that used it. Use: a bind's lookup (tex_fast_rid, not a pool walk's) and a shader's read through a pool, whose bitmap's folded-in bits are now cleared every 10 s so a read sets its bit again (g_tid_used). An evicted ID has its read bit cleared, and a pool walk leaves it the dummy until a shader reads it; a bind makes it again at once. The trim's log says what it freed and why the rest stayed. DM_CTL memtrim asks for a trim on the Mac. Mac (16 GB M4, the field route, guest in files): two trims on the field freed 481 textures (119 MB) and then 577 (215 MB), the set 3,487 → 3,163 MB; 24 packages were made again between them, and the screenshots after each show the owl's scene whole. t_pkg_evict: the idle copy leaves the set, its entry reads the dummy, the walk does not make it again, a read does; a texture used lately, one the GPU wrote and a pinned one stay; nothing goes without pressure. With the walk's gate off it fails. On this branch: a freed texture's pool table entry holds its stand-in when it has one (pool_stand), else the dummy, and a package whose storage took GPU stores (stored) stays, as a GPU-written one does. Rebased onto main's resource owner (nvn_owner.m, 96259d4): the packages are found with own_each_locked, and their serials' entries, live and finalized, go through a new own_drop_where_locked, which drops each as serial_del does and frees a finalized serial's ring slot (the walk over g_by_serial and g_ghost it replaces left the slot behind). A pin, the sharing table (own_shared_locked) or a serial holding the texture (entry_holds) keeps a package as before.
…reed at memory pressure, and what stays of the raw pool table's pools
…s before each, and two of the owner's runs ended at the castle One driven process ran 60 rounds of the castle town and the field with a memory pressure trim before each load (2.4 hours, 152,019 idle uploaded textures freed and made again), at most 6.29 GB wired and the set at 4.43 GB. The owner's first run stopped drawing at the castle entrance's load with the sound on, the residency commit freeze's signs; the second ended at the castle gate with no report. Both are open in the game note.
…r rows in docs/env_switches.tsv
…pipelines through the Metal 4 path with the pipeline archive made offline and the hybrid pipelines Through the MTLDevice, each render pipeline first drawn in a scene state the phone had never drawn compiled on the submit thread: on an iPhone 17 in Kokiri Forest after the first boss, about 17 pipelines of about 400 ms each made 8 frames of 0.17-1.26 s (6.0 s in all), every wait "hybrid off". The archive for applegpu_g18p holds every pair's pulling base (30,690) and a pipeline per compute shader (4,125): 3,501 MB, 28.5 min of metal-tt on the Mac. From the experiment commit 7e5d12c6 (the game.tsv lines), on main's key: main has the launcher's setting (DM_PSO_MTL4) and the fix for pipeline bases missing from the archive (2f131ed, the experiment's 3af843f3), not this profile's default. The game note's "For an iPhone or iPad build" says so, and dm/games/README.md's pso_mtl4 row names the game. No shader input changes (pso_mtl4 is no shader_* / nvn_* key).
The warning that a texture which left out its largest level could not be made whole again (iPhone memory 1, tex_undrop) used RED_ONCE key 160, which main's geometry capture took in the meantime for its vertex stream in no memory pool (nvn_geometry.inc). Each one set the other's flag, so whichever fired first silenced the other for the run. The texture warning now takes key 168 (main has since taken 166 and 167 for its buffer images), and g_red_once grows to 169. Checked: no RED_ONCE key is now used by two different warnings (git grep against main), the host builds. Not run: a game (a log key).
stan69b
force-pushed
the
pr/oot-iphone-smaller-textures
branch
from
November 12, 2026 19:26
fb30479 to
b8cb0bd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1971. This branch holds #1971's commits (head
4283410d) until that pull request merges. The commits of this pull request are the 28 after it: seven for smaller textures, staging and MetalFX, 19 for memory pressure (iPhone memory 2aon), the Metal 4 default, and a log-key fix. Review those alone withgit log d9a9007b..pr/oot-iphone-smaller-textures.Rebased onto main
0c078130with #1971 (2026-11-12)Rebased with #1971 onto main
0c078130(headb8cb0bd6). Authors and dates are unchanged, and each re-fitted commit says what changed in a paragraph at the end of its message.0c078130) and its second source buffer only for a changed batch (0c23e0b5). The upload budget commit records the most file-backed bytes wired before one commit whereres_committakes the wired ranges, and the largest set after a commit underg_res_commit_mu, where main reads the set. The first source buffer may flush and begin a batch (its budget), so the batch that holds it is read afterup_big_src; the second is made, without splitting, only whenup_allocflushed that batch.t_upload_srcturns the budget off (DM_NVN_UPLOAD_SRC_MB=0): with it, each of its 64 MB textures starts a batch of its own, which its count of source buffers does not expect;t_upload_budgettests the budget. The memory-pressure report is checked after each commit once the commit's lock is released, and the per-line counts sit beside the queued changes.RED_ONCEkey 160, which main's geometry capture took meanwhile (nvn_geometry.inc), so whichever fired first silenced the other. It now takes 168: main has since taken 166 and 167 for its buffer images. No commit was dropped, and authors and dates are unchanged. Each re-fitted commit says what changed in a paragraph at the end of its message, and every commit along the way compiles.nvn_owner.m, 96259d4) now holds the serial and sharing tables. The idle-texture eviction finds packages withown_each_locked, and drops their serials' live and finalized entries through a newown_drop_where_locked, which also frees a finalized serial's ring slot. The census asks the owner who holds a texture (own_shared_locked, plus two new accessors,own_ghost_holds_lockedandown_alias_seen_locked).pkg_written,up_copy_add): a texture that leaves out its largest level is re-uploaded whole, because those images and copies count Metal levels from the game's level 0.tex_undroptakespkg_upload's eight arguments andown_note.rh_trim_nsits on main'srh_heap/rh_place, and the spare's note sits besideown_note.pso_mtl4 on(from the experiment 7e5d12c6). Main already had the launcher setting and the fix for bases missing from the archive (2f131ed). The game note anddm/games/README.mdsay so.Tests on the rebased head (M4 Mac):
t_pkg_evict43,t_pkg_spare59,t_rt_recycle,t_pool_standin31,t_tex_drop_mip184,t_upload_slots307,t_upload_budget261,t_upscaler_mfx24,t_direct_upload26,388,t_view_levels45,t_pfv,t_upgrade_alias13,t_window_alias25,t_res_absent, and main'st_dispatch_barrier31,t_res_commit8,t_upload_src41.test_docs,test_env_switches,test_oot_aspect,test_game_profile,test_nsp_chain,test_settings: all OK.b8cb0bd6(base0c078130).The regression check below ran on this base (main
0c078130, headb8cb0bd6).What and why
On an iPhone 17 (8 GB, Metal working set 5,461 MB), Ocarina of Time at the launcher's 30 fps cap paged its guest memory back from disk all through play: 24 fps, 110 late frames per 300, frames over 60 ms in 47 of 56 windows. The cause was memory, not GPU time. These changes free memory on small devices, swap the game's own upscaler for MetalFX, and add an opt-in pixel bound. With them the phone holds the cap, and the owner played through the Great Deku Tree to the Kokiri Emerald.
iPhone memorycommits, plus their docs). Where Metal's working set is under 6 GB, large sampled 2D textures made from the game's data leave out their largest level. That applies to textures with both sides ≥ 512, 2+ levels and nothing rendering into them. Only sampling sees the smaller texture; any other use makes it whole first.DM_NVN_TEX_DROP_MIP=0/1forces it off or on. 8 GB Macs fall under the 6 GB rule too; none was measured with it.UPSCALER_SOURCEnow also takesagl_ndsr_vr(_light).pck. That pack's networks read a square-root-encoded input, which the host recognises from the code (signed squares,HMUL2 R, R, |R|). It squares the input in a small pass, and MetalFX scales it in HDR mode. Variants that encode their output to sRGB keep the game's shader.DM_NVN_UPLOAD_REUSE=0/1still forces either way.DM_SCREEN_PIXELS(opt-in, 0.5–1.5) holds a non-16:9 picture to that share of the game's pixels. At 1, an upright iPhone 17 renders 976x2120 instead of 1200x2600. This port also fixes the shape log line's if/else, which would otherwise log "screen shape 16:9" again after every uncapped shape.iPhone memory,2and2a/2bcommits). A memory pressure event (on iOS warn and critical, on the Mac critical only) now does three things: it trims what no frame needs (retired render-target heaps, upload staging), logs the system's free, wired, compressed and file-backed memory, and describes what the residency set holds. Each upload batch gets a budget for the zero-copy buffers over guest memory it wires: 96 MB, set withDM_NVN_UPLOAD_SRC_MB. Without it, the field's load wired 732 MB of file pages within seconds on the iPhone 17. The census now counts a heap once.DM_NVN_MEM_PRESSUREchooses log only, trim at warn, or trim at critical.DM_RES_PROBEruns a probe: a read through a pointer to a buffer outside the residency set returns zeros.DM_CTL memtrimasks for a trim. Adapted to this branch: a freed texture's pool table entry holds its stand-in when it has one, else the dummy, and a texture whose storage took GPU stores (stored) stays, as a GPU-written one does. These two points were not in the build tested on the phone.Docs: the
memory.mdentries for each of the above, the two upscaler entries inrendering.md,settings.md's Game's upscaler,env_switches.tsv, the symptom index and the game note.Numbers (iPhone 17, frames 6,000–22,500, 30 fps cap, dynamic resolution on)
prmem_offprmem_onr1_baser1_reuse0b2_play1, hand play): the 5 networks met were drawn by MetalFX, and the 3 others kept their shader. Scalers ran from 943x434 up to 1508x695 → 2120x976, and the 30 fps cap held while dynamic resolution rose. The owner saw no wrong picture ("maybe deeper colours"). There is no frame-time A/B against the game's network yet.DM_SCREEN_PIXELS=1: the owner called it "very smoothly, practically locked 30 fps". No A/B was run, so the GPU saving (about 1–1.5 ms) is an estimate.These device numbers come from the
iphone-expbuild. That build holds the same changes on an earlier base (chain39eb2f781d07), with upload staging off on every device instead of gated by the 6 GB rule. No iOS build was made of this branch: this branch's chainbc8830d4ba5ahas no iOS shader libraries, and building them takes about 2.5 h.Memory pressure on the iPhone 17
The build was
iphone-mem3: these commits on theiphone-expbase, before the stand-in adaptation.Driven run (one process,
DM_CTL, from a drawbridge autosave): 60 rounds of the castle town and the field with a trim before each load, 120 loads in 2.4 hours (177,611 frames). It did not end, and every screenshot was clean. The run had 77 pressure warns and no critical. 206 trims freed 152,019 idle uploaded textures (42.9 GB in all). Wired at most 6.29 GB, free down to 33 MB, the set at most 4.43 GB (3.82 GB at the end). Frame p50 36–50 ms across the 300-frame windows.The owner's runs from the field save:
iphone-mem2)The owner's other run stopped drawing at the castle entrance's load with the sound playing on. That matches the residency commit freeze in
memory.md, but it is not confirmed. Both are open in the game note.On the Mac's field, two trims freed 119 MB and 215 MB (set 3,487 → 3,163 MB).
Tests
make -C dm/gfx/tests GAME=0400486024DEC000passest_tex_drop_mip(184 checks),t_pool_standin,t_direct_upload,t_view_levels,t_pfv,t_upgrade_alias,t_upload_slots(also withDM_NVN_TEX_DROP_MIP=1),t_upload_budgetandt_upscaler_mfx.check_t_upscaler_mfxalso passes under Donkey Kong Bananza (040074A01BF12000) and Splatoon Raiders (0400380020056000).test_docs,test_env_switches,test_oot_aspect,test_game_profileandtest_settingspass.t_pkg_evict(43 checks),t_pkg_spare(59) andt_rt_recycle(25) pass on this branch. So dot_pool_standin,t_tex_drop_mip(184) andt_upload_slots(307), re-run after the merge.test_docsandtest_env_switchespass, and the host builds.test_env_switchesstill prints the existing note thatDM_NVN_COPY_ABSORBis quoted innvn-objects.mdbut read nowhere.Regression check
scripts/regress.py check --games 0400486024DEC000, baseline main0c078130, candidateb8cb0bd6(this head), on an Apple M4 running macOS 27.2 with Xcode 27.2. It ran with the side games folder (--games-dir …/ipad/regress-games), so the owner's build folders were not touched. Result: no change in the shaders, and no picture change. The candidate starts faster and its frame p99 is lower.Run 2 (
20261112-222232-check, 1,561 s), the one that counts:0400486024DEC000Run 1 (
20261112-215519-check, 3,780 s) said NEW-FAIL, but this branch did not cause it. The candidate's pipeline prebuild stalled for about 120 s under load. It did not exit within the run (1,200 s, against 1,021 s for the baseline), and its prepare step took 38.46 s (baseline 10.84 s). The first frame came at 47.9 s, and the screenshots then differed for the same title-timing reason (dHash 19 and 27 bits). Shaders, MSL and RED lines were unchanged there too (42848/42848, 0 changed, 0 new RED). Run 2, under normal load, did not stall.regress.py impactnames Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom, and none of them is on this Mac. The Vulkan profiles never reach the NVN paths this pull request changes. Bananza and Splatoon Raiders, the other games with the upscaler, are covered bycheck_t_upscaler_mfx.Related
DM_NVN_CPUPROFreport, which is empty with the encode thread on.alias_take_overCPU.Open
Memory pressure: what ended the owner's iPhone run at the castle gate, and whether the stall at the castle entrance was the residency commit freeze. The stand-in path of a freed texture has not run on a device.
Upload staging: release it only once uploads go quiet, which would keep reuse for a small device's loads.
MetalFX: an A/B of frame time and picture against the game's network.
First visits: a scene state's first visit freezes while pipelines build on the submit thread (1.4–2.0 s).