Repository navigation
Soft textures: a large block-compressed texture kept without its finest level, its level-0 pages back - #1599
Open
oliverames wants to merge 8 commits into
Open
oliverames wants to merge 8 commits into
oliverames wants to merge 8 commits into
Conversation
oliverames
force-pushed
the
pr-soft-textures
branch
3 times, most recently
from
November 7, 2026 20:19
8b69d92 to
e968204
Compare
cfryerdev
pushed a commit
to cfryerdev/nxnative
that referenced
this pull request
Nov 7, 2026
…a, gmem commit repoint on the test stack seedds#1589 (carrying seedds#1391 + seedds#1482) the GPU memory guard reads the residency set's size and stops before macOS runs out of RAM; hand-merged with seedds#1590's ghost-active alias tables. seedds#1599 soft textures (gated, default off). seedds#1662's tip commit: the ocarina plays into the Mac's microphone (DM_MIC=1). seedds#1427 raw pool table words follow the pools at commit, as a candidate for seedds#1326's page faults (its window is skipped, not faulted: kept for the record). Full-map shader library (42,848), clearing the no-stamp and c1_full red lines. In-game: the GPU page fault at scene changes (seedds#1326) remains on this stack - one session lost the picture at frame 55,940 with the device revoked; the fault predates seedds#1585, which was excluded after the M2 Pro report but is not its cause. Front-ends: host change only, chain 2027aa29b1b7.
Contributor
Author
|
Rebased onto main 7f50222: two conflicts in nvn_mtl.m (mtl_sub_begin kept main's line without the removed g_rc_npend/g_rc_skip plus soft_sweep(); tex_get kept main's tex_pin version). Tests: t_dirty_slots, t_dirty_race, t_dirty_retire_race pass; t_link_ticks (compiles nvn_mtl.m) 40 checks, 0 failed; test_docs 11 OK. No GPU run. |
oliverames
force-pushed
the
pr-soft-textures
branch
from
November 8, 2026 22:30
e968204 to
83ef0da
Compare
cfryerdev
pushed a commit
to cfryerdev/nxnative
that referenced
this pull request
Nov 8, 2026
… upload diagnostics kept)
oliverames
force-pushed
the
pr-soft-textures
branch
from
November 10, 2026 01:10
83ef0da to
cc6b19f
Compare
2 of 3 tasks
oliverames
force-pushed
the
pr-soft-textures
branch
from
November 10, 2026 03:20
cc6b19f to
c8e6049
Compare
added 4 commits
November 10, 2026 07:51
…its files is kept without its finest level, and its level-0 pages go back A first upload of such a texture makes a host texture of its level 1 (Mario Kart World keeps large BC formats from its files with a packaged layout; the host texture that skips level 0 is sampled through the same normalized coordinates), and the guest pages only level 0 holds go back after 300 frames (nvn_dirty_release) unless the game copies from that texture (soft_harden keeps it whole, as Mario Kart World copies source textures into its terrain arrays; the kartnative line notes a copied-from texture that lost level 0 drawing purple). iOS wires the whole memory pool behind a zero-copy buffer, so these uploads run on the CPU while the pool has none (mtl_pool_wrapped) instead of handing the pool a buffer it never addresses (the kartnative/plumber-racer-2 measurements: wrapped pools 3.7 GB -> 851 MB there). The mechanics: nvn_dirty_release gives back only still-protected whole pages whose bytes the host keeps (read-only again after the madvise dance, then given all-zero or left as-is); with the guest in files (DM_GUEST_FILE) there is nothing to give back (dm_guest_file_backed). The pipeline: tex_soft gates the descriptor (level shift sk) in tex_desc, and every upload reads the source one level down (pkg_upload_, CPU deswizzle included); the copy hooks (nvnCopyTexture t2t/t2b) harden the source first (soft_harden, then a pkg_upload of the whole levels); ReadTexels sees one lower level (tex_soft_level, an unmapped level-0 read reads 0). All default-off: the switches are DM_NVN_TEX_SOFT and DM_NVN_TEX_RELEASE, so no game changes until its profile names tex_soft; the sweep runs once a submission (mtl_sub_begin) and leaves a 300-frame grace (soft_sweep), as kartnative's does. Where it came from: the kartnative (pahorema) and plumber-racer-2 (cheemu21) forks (MIT) had this first, for Mario Kart World on an iPhone 18 Pro in LiveContainer at about 3.9 GB. The port keeps their behaviour and takes this tree's own anchors (tex_desc, pkg_upload_, the copies, ReadTexels, the dirty layer). Measured on an iPhone Duo (iOS 27.3) through the seedds#1383 family's same anchors: a full free-roam session at 3.7-3.9 GB where the same content without the switches sat at 4.5-5.3, and the whole-frame record lost its p95 spikes (51/81 ms p50/p95 with the switches as against 57/215 without). The tests in this PR run the CPU-only layer here; the GPU checks and the phone runs are recorded on the source lines.
…e, with the kartnative/plumber-racer-2 origin noted
…_TEX_SOFT and DM_NVN_TEX_RELEASE get their rows in the switch table tex_soft_release assigned realloc's result to g_soft_pend itself (clang-tidy bugprone-suspicious-realloc-usage); it now takes the new block and the new capacity only when the allocation succeeded, and dies as before when it failed. docs/env_switches.tsv gives both switches a class and a meaning (test_env_switches.py). Two comments named the guest-file case wrongly: the switch is DM_GUEST_FILE, and nvn_dirty_release gives nothing back while the guest is in files.
oliverames
pushed a commit
to oliverames/nxnative
that referenced
this pull request
Nov 10, 2026
…me on an iPhone, and the phone restarts some minutes later (Mario Kart World 1.8.0, iOS 27.3 beta); open On an iPhone 19,4 the picture stopped 2 to 3 s into a race, 184 s after launch, with the overlay at 0 fps, the process alive, no [RED] line and no GPU error. Every thread waited and none spun. The state dump has the replay thread in iokit_user_client_trap under res_commit (pool_res, with g_mu held by its caller and g_res_mu across the call) and the GPU's completion queue waiting for a mutex in post_complete, with the main fence at submitted 206262 / completed 206256. Ending the app did not end the process. The system's stack report 15 minutes later still has it with one thread, NVN queue replay, in an uninterruptible kernel wait with a read-write lock held and a kernel thread blocked on it, and the phone at 7.2 GB wired and 34 MB free. The phone then restarted itself. It happened twice in four runs of one build (main 04811a6 with PRs seedds#1709, seedds#1695, seedds#1262, seedds#1599 and seedds#1713 merged): in that race, and an hour later at the title's demo with no button pressed, 353 s after launch, with the same thread state. Both came in a scene load (1,840 allocations added to the residency set in 678 commits in the 300 frames before the second). With main's 6f50de0 and 12a44e0 added, four races did not freeze, which is too few to say it is gone. The game makes about five residency commits a frame in a race. No fix: the entry says what was ruled out, what the host could change, and what to collect from the phone (SIGALRM for the host's dump, then systemCrashLogs). The game note links it under Open.
oliverames
force-pushed
the
pr-soft-textures
branch
from
November 10, 2026 13:04
c8e6049 to
26ee876
Compare
…ad on the CPU reads the level it sizes
With DM_NVN_TEX_SOFT=1, a region the game copied on the GPU into a large block-compressed texture showed one mip level off, or was gone at the texture's next upload, with "[RED] GPU write into ser=... not written into its storage (the host texture does not hold its bytes)" in the log. A copy into the texture followed by a copy from it in one command buffer read the texture without the first copy.
The host texture of a soft texture is half the size and holds the game's level n + 1 at level n. A buffer-to-texture copy (copy_b2t), a texture-to-texture copy (copy_t2t_enc) and a copy through an uncompressed alias (bc_alias_copy) took the game's level number as it was. The buffer copy still reached the storage, with no upload to follow it; the texture copy did not, as gsb_store takes only a host texture of the storage's size (gsb_host_ok). Only a texture the game copied from was made whole.
soft_harden now runs on the destination of those three copies before the copy is encoded, as it does on a source: the texture is uploaded again with its finest level and stays whole, so the copy takes the ordinary path and reaches the storage. Its upload has no held GPU store to wait for (gsb_held): a soft texture's storage gets none, as every copy into it comes there first.
A texture whose finest level's storage has gone back (DM_NVN_TEX_RELEASE=1) cannot be made whole and stays soft. A copy from or into its level 0 is skipped, and a copy at another level takes the level one lower in the host texture (tex_soft_level), where the sizes agree. A buffer copy into it still reaches the storage; a texture copy into it does not and is reported by the red line above. Before, these copies used the game's level and sizes on the half-size texture: Metal's validation stopped the test on a texture-to-buffer copy.
The test also found the upload on the CPU, which a soft texture takes while its pool has no zero-copy buffer, empty: the loop sized each level's rows for level l + 1 and deswizzled level l, which nvn_deswizzle refuses ("[RED] deswizzle failed"). It now deswizzles level l + sk, as the upload on the GPU encodes it.
copy_b2t and copy_t2b return at once for a copy with no texture, which had no host texture and returned a line later: with the new calls clang-tidy followed that path to the lines between.
Checked: the new t_soft_texture, 172 checks: the half-size upload and its level shift on the GPU and on the CPU, a CPU write uploaded again the same way, a buffer copy, a texture copy and an alias copy into level 1 (the texture whole afterwards, the region at level 1 and not at level 2, the storage's bytes, the region still there after the next upload, no red line), a copy from level 0, a copy in and a copy out in one command buffer, and the released case. On the tree before this commit 19 of its checks fail, among them both copy cases, and Metal's validation stops it in the released case. docs/reference/soft-textures.md has the two entries, the symptom index their lines, and the two switch rows say what a copy does.
oliverames
force-pushed
the
pr-soft-textures
branch
from
November 10, 2026 13:09
26ee876 to
218fd09
Compare
This was referenced Nov 11, 2026
1 task done
added 3 commits
November 12, 2026 12:24
Conflict: dm/gfx/nvn/nvn_mtl.m, five hunks. - mtl_pool_del: main moved the body to pool_take_out / pool_gone_release; kept the PR's mtl_pool_wrapped beside them. - mtl_sub_begin: main's nvn_dirty_touch for g_pkg_kept plus the PR's soft_sweep. - NvPkg: main's own, lost_gen and part fields plus the PR's released. - pkg_upload_: main's partial re-upload (need, pkg_written, up_copy_add) with the PR's level shift sk and keep_cpu in the staged total and the source buffer (src_batch). Adaptations: - pkg_written takes sk and asks about the byte range of the game's level l + sk, so a soft texture's partial re-upload reads the rewritten level and copies the others from the texture it replaces. Without it a written level 2 left host level 1 stale. - pkg_upload's forward declaration and soft_harden's call take the new arguments. - t_soft_texture: the CPU-write case writes only the level's own pages, so the partial path is taken, and checks it (175 checks). - docs/reference/soft-textures.md names the partial re-upload.
…its host level l always holds the game's level l + 1 (PR seedds#1599) The direct path (nvn_deswz_gpu_direct_m4) reads the game's level l into host level l and had no level shift, while the CPU and staged paths read l + sk. A soft texture in a wrapped pool could have reached it and been uploaded one level off. None can today (tex_soft wants a block-compressed format, the direct path RGBA16F up to 64x64), so this keeps the two apart on purpose: refusing is one term in the selection, and shifting would touch the kernel's per-level offsets and the all-levels dispatch for a case that does not exist. Checked: t_soft_texture 190 checks 0 failed (175 before; the new block holds the selection and each host level), t_direct_upload 26388, t_partial_upload 3585, t_upload_src 41, test_docs.py OK, compile_check.py 263 files 0 failed. The new test passes before the fix too, as no soft texture is direct-eligible; it guards the pairing.
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.
What and why
A large block-compressed texture a game loads from its files today becomes one full-size Metal texture and keeps the whole guest storage wired. The kartnative and plumber-racer-2 forks (MIT) solved this for Mario Kart World on the iPhone, and this carries their soft-texture family into the tree on its own anchors:
tex_soft(gated, default off) makes such a texture's host texture from its level 1 (half the size, one level less, sampled through the same normalized coordinates).nvn_dirty_releasegives back the guest pages only that level holds after a 300-frame grace (soft_sweep, run once a submission), unless the game copies from that texture (soft_hardenre-uploads it whole first; kartnative's line records a copied-from texture that lost level 0 drawing purple).mtl_pool_wrapped): iOS wires a buffer's whole pool, for a pool the GPU may never address (their measurement: wrapped pools 3.7 GB → 851 MB on an iPhone 18 Pro).ReadTexelsis answered from the texture's storage, so it gets the level the game names; a level-0 read of a released texture reads 0.No game changes unless
DM_NVN_TEX_SOFT=1is set (andDM_NVN_TEX_RELEASE=1for the release). There's no profile key for it, so everything here is opt-in.GPU copies into a soft texture work as of 2026-11-10. A texture that a GPU copy writes into is made whole first, the same way one that's copied from already was, so the copy lands at the level the game names and reaches the texture's storage. That covers buffer-to-texture copies, texture-to-texture copies and copies through an uncompressed alias. Before, they landed one mip level off in the half-size host texture, and since main's
ab41d6a17a texture copy never reached storage, so the next upload dropped it.One case can't be made whole: with
DM_NVN_TEX_RELEASE=1, a texture whose finest level's storage has already gone back stays soft. A copy from or into its level 0 is skipped with one red line. A copy at another level goes to the level one lower in the host texture. A buffer copy into it still reaches storage; a texture copy into it doesn't, and the log says so.A released level is gone from the game's own memory too: once the system has taken the pages they read as zeros and nothing puts the bytes back. The comment on
nvn_dirty_releasesaid a later write brought them back, which the code never did; the comment and the reference entry now say what happens.Still open: a texture view that starts above level 0 isn't shifted, so it shows one level coarser. The GPU direct upload (
nvn_deswz_gpu_direct_m4) can't take the level shift, so it is now refused for a soft texture (!skin the selection,f72cc280); no texture qualifies for both today (the direct path takes RGBA16F arrays of at most 64x64, a soft texture is block-compressed and at least 512x256), so this guards the pairing rather than fixing a live case.t_soft_texturechecks it with a soft texture in a wrapped pool and the direct upload on: no direct upload, each host level holds the game's next level (190 checks). That's from reading the view lookup, not from a test, and the reference entry says so.Where it came from:
tex_soft,nvn_dirty_release,mem_map_limitlines).pr-1383-deployof my fork carries the same diff).Docs:
docs/reference/soft-textures.mddescribes the mechanism anddocs/env_switches.tsvhas both switches. If this lands, the game-note rows (dm/games/<id>/README.md, memory section) take the settings and the numbers game by game, as the fork lines already document for Mario Kart World.Checked
Tests and checks:
0c23e0b59merged in (head18454f81). Main's partial re-upload (d2556cffa) asked about the written bytes of the host level, which for a soft texture holds the game's level one finer, so a rewritten game level would have left its host level stale; it now asks about the game's level, andt_soft_texturegained 3 checks for that path (fails with the shift forced to 0). On that head:t_soft_texture175,t_partial_upload3,585,t_direct_upload26,384 and 26,388,t_cpu_tex105,t_upload_slots305,t_upload_src41,t_serial_owner176 checks, all 0 failed;test_docs,test_env_switches,test_gfx_tests_makefileand the iOS compile check (263 files) pass.t_nvn,t_readback,t_cpu_storageandt_bc_aliasweren't rerun.25c2546eon 2026-11-10 (head218fd092), on an Apple M2 Pro Mac, withGAME=040074A01BF12000where a test takes a game.t_soft_texture(new): 172 checks, 0 failed. It covers the half-size upload and level shift on the GPU and the CPU, a CPU write, a buffer copy, a texture copy and an alias copy into a soft texture, a copy from one, a copy in and out in one command buffer, and the released case. Before the fix, 19 of its checks failed and Metal's validation stopped it.[RED] deswizzle failed). That's fixed in the same commit. The phone runs below were on the WIP: Mario Kart World 1.8.0 on iOS - rendering, memory and presentation #1383 tree, which has the shift, and their log has no such line, so those numbers stand.t_nvn: 298 checks, 0 failed.t_readback: 217 and 218 cases, 0 failed.t_direct_upload: 26,384 checks, 0 failed.t_cpu_storage: 135 checks, 0 failed.t_bc_alias: 19 checks, 0 failed. The readback, direct upload and CPU storage tests give the same counts withDM_NVN_TEX_SOFT=1 DM_NVN_TEX_RELEASE=1.t_dirty_slots,t_dirty_race,t_dirty_retire_race: pass.make -C dm/hostbuilds.test_lint_c.py(19 tests),test_docs.py(6) andtest_env_switches.py(1) pass, and the symptom index is current.main: nothing above the baseline.nvn_dirty_releasetouched no other caller; withDM_GUEST_FILEand the guest in files,dm_guest_file_backed()makes it a no-op (the guest's memory is not the process's).scripts/regress.py checkover the local games. The local NSP folders no longer carry their shader/build run assets (they went to the Trash in this workspace's sources-only cleanup on November 5, 2026, so the harness discovers no Mac builds); CI's queued runs (host unit tests, CPU-only tests, tool tests, build check, sanitizers, coverage, unsigned iOS app) cover the change. Happy to run the local check once the game data is restored from the Trash.Games run, with title ID and version: Mario Kart World 1.8.0 (
0400C3F00006E000).Devices and OS: iPhone Duo (iPhone19,4, iOS 27.3); the GPU checks ran on an M2 Pro with Metal 4, macOS version not stated.
scripts/regress.py check --games: not run, see the "Not checked locally" line above.Measurements before and after (iPhone Duo, iPhone19,4, iOS 27.3, Mario Kart World 1.8.0, the #1383 family's same anchors; a full free-roam drive of 4.2 min, auto-accelerate, with the switches on against without):
[RED]count 38 in the window (the driver-side MTL4 victim discards that every lane reports open, unchanged by this feature: recorded in the same session's log for whoever takes that lead).Log excerpts of that run (boot ramp + the driven window; the released-pages line arms after SoftWait):
The full run's
log.txt/memory-budget.tsv/memstat.tsvand the keep-side run they compare against are filed in the parallelanygame track tree (logs/pr1383-mkw-run-20261106/driven3-*anddriven2-*); happy to attach the full text here if you prefer.Front ends
None: no front end has to act on this.
Docs
docs/reference/soft-textures.mdentries, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index is in the diff).dmsh_chain.py --headerwas run anddm/gfx/nvn/dmsh_chain.his part of the change. Not applicable: no chain tool changed