Let a finalized slot sample its own texture's image while that still holds the texture, not the stand-in.

The game finalizes texture objects on its own thread ahead of the replay, which still draws the frame the game recorded
before that; on the console the memory still holds the texels those draws read. Kakariko moves about 30 of its
textures to new addresses every four or five frames, and each move finalized the old descriptors. The replay's draws
of the frame before then met finalized CPU-written slots, and the stale rule gave them the stand-in even when their
own entry was still there with its content. The village's haze dropped out of a frame a few times a minute, and the
haze sprites that met a stand-in left hard-edged clear patches. A play session in Kakariko logged up to 4,805 stand-in
lookups in 60 frames, 1,826 in the 60 frames around one haze dropout, and 612 in the 60 frames after a run of
patched frames.

Such a slot now resolves to its own entry while that entry was uploaded or drawn by the GPU, and was neither superseded
by another descriptor at its address nor overwritten. The lookup samples the image as it is: it neither takes the
entry back nor reads its storage, so freed storage is still never read (the access violation the stale rule was made
for). NX2NAVI_TEX_STALE_KEPT=0 gives the stand-in again. The perf log's finalize line counts these lookups.

In a replay of a recorded Kakariko walk, most 60-frame windows went from stand-ins to the kept image (up to 328 a
window, stand-ins 0). In play, stand-ins remain in short bursts where the entry was already gone (up to about 2,000 in
60 frames, against the kept image in most windows). The tester still sees flicker that the 640x360 frame recorder
does not catch; that is not settled by this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-12 05:42:42 -05:00
committed by tangerineturtle1
parent 4b87606f32
commit 4956ef8365
3 changed files with 32 additions and 12 deletions

View File

@@ -100,9 +100,11 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
(`vk_exec_draw.c pool_sync`); slots that resolve alike share one lookup.
Finalization is retained on registered slots independently of image-cache eviction: a stale CPU-backed descriptor
cannot recreate an image from freed storage or follow newly reused CPU bytes. Registration makes it current again;
finalized descriptors may still follow live GPU-owned content. A finalize marks only the slots its own texture
object registered (by initialization serial): the game initializes and registers the next terrain heightmap at the
storage of the previous one before it finalizes that one (`vk_texture.c vk_texture_release`).
finalized descriptors may still follow live GPU-owned content, and sample their own image while it still holds
the texture (`vk_texture.c stale_kept`): the game finalizes ahead of the replay of its earlier frames. A finalize
marks only the slots its own texture object registered (by initialization serial): the game initializes and
registers the next terrain heightmap at the storage of the previous one before it finalizes that one
(`vk_texture.c vk_texture_release`).
- Shaders: Ampere (SM86) code is lowered to Maxwell (`tools/xlate/AmpereLowering.cs`) and translated to SPIR-V by the
pinned shader translator (`tools/vendor/ryujinx`, 1.3.2) with our patches (`tools/xlate/patches/`), built with .NET 10 NativeAOT as
`nx2navi_xlate.exe` and run as separate processes. Translations are cached on disk; shaders and pipelines seen in

View File

@@ -58,7 +58,7 @@ static const char *const known[] = {
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_CLICK", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOUSE_TRACE", "MOVIE_HOST", "MOVIE_PACE", "MOVIE_CLOCK", "TEST_HITCH",
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
"MV_SURVEY",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_RES_FIXED", "PATCH_SCENE", "PATCH_WIDE_ASPECT", "PATCH_WIDE_HUD", "PATCH_WIDE_MOVIE", "PATCH_WIDE_OUT", "PATCH_FREECAM", "PATCH_WIDE_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPELINE_CACHE_MAX_MB", "PIPELINE_CACHE_PLAY", "PIPELINE_CACHE_PLAY_MB", "PIPE_BINARIES", "PIPE_BINARIES_MAX_MB", "PIPE_FEEDBACK", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT", "POWER_FULL", "TEX_SAME", "TEX_FLAT3D", "TEX_KEEP_TAKEN", "TEX_RELEASED_MAX", "PRESENT_HELD", "HELD_SYNC",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_RES_FIXED", "PATCH_SCENE", "PATCH_WIDE_ASPECT", "PATCH_WIDE_HUD", "PATCH_WIDE_MOVIE", "PATCH_WIDE_OUT", "PATCH_FREECAM", "PATCH_WIDE_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPELINE_CACHE_MAX_MB", "PIPELINE_CACHE_PLAY", "PIPELINE_CACHE_PLAY_MB", "PIPE_BINARIES", "PIPE_BINARIES_MAX_MB", "PIPE_FEEDBACK", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT", "POWER_FULL", "TEX_SAME", "TEX_FLAT3D", "TEX_KEEP_TAKEN", "TEX_RELEASED_MAX", "TEX_STALE_KEPT", "PRESENT_HELD", "HELD_SYNC",
"PREPARE_PIPELINES", "PREWARM_FULL", "PREWARM_WAIT_MS", "PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES", "PROFILE_REPLAY", "PROFILE_SLOW",
"POOL_FAST", "POOL_GROUP", "POOL_GROUP_CHECK", "POOL_PRESNAP",
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",

View File

@@ -4,7 +4,8 @@
// and sampler descriptor slots, and VkSamplers.
//
// A texture that mirrors its pool memory (tex_mirrored) stops re-uploading once the GPU rendered into it (the
// "GPU-owned" rule); a finalized slot follows live GPU content but never revives freed CPU-backed storage.
// "GPU-owned" rule); a finalized slot follows live GPU content or samples its own image as kept, but never revives
// freed CPU-backed storage.
// Contains code Copyright (c) 2026 jhaizhou, Copyright (c) 2026 Peiikai (MIT; see THIRD_PARTY_NOTICES.md).
//
// Every image lives in VK_IMAGE_LAYOUT_GENERAL. Uploads are recorded into the upload batch of vk_memory.c; the replay
@@ -1550,7 +1551,7 @@ static int resolve_stale;
// with the same parameters registered with another texture object, which a finalize leaves current
// (vk_texture_release), and lookups the stale rule answered with the stand-in (slot_resolve). _hm: of them, the
// terrain's streamed heightmaps (R32F 260x260), whose stand-in draws a chunk of ground at height zero.
static uint64_t n_fin_own, n_fin_other, n_fin_other_hm, n_stale, n_stale_hm;
static uint64_t n_fin_own, n_fin_other, n_fin_other_hm, n_stale, n_stale_hm, n_stale_kept;
static int terrain_heightmap(const tex_t *t) {
return t->d.format == NVN_FORMAT_R32F && t->d.width == 260 && t->d.height == 260;
}
@@ -4086,6 +4087,20 @@ static int want_take(void) {
void vk_texture_create_budget_locked(int n) { create_budget = n, create_deferred = 0; }
int vk_texture_create_deferred_locked(void) { int d = create_deferred; create_deferred = 0; return d; }
// NX2NAVI_TEX_STALE_KEPT (default 1; 0: the stand-in, as before): a finalized slot of a CPU-written texture resolves to
// its own entry while that entry holds the texture's content: uploaded or drawn by the GPU, and neither superseded by
// another descriptor at its address nor overwritten. The game finalizes texture objects on its own thread ahead of the
// replay, which still draws the frame the game recorded before that; on the console the memory still holds the texels
// those draws read. Kakariko moves about 30 of its textures to new addresses every four or five frames: each move
// finalized the old descriptors, the replay's draws of the frame before then met finalized slots, and their stand-ins
// took the village's haze out of a frame a few times a minute (thousands of stand-in lookups in 60 frames). The image
// is sampled as it is: the lookup neither takes the entry back nor reads its storage (vk_texture_by_id_locked).
static int stale_kept(const Tex *own) {
static int on = -1;
if (on < 0) on = rt_knob("TEX_STALE_KEPT", "1") == 1;
return on && own && !own->superseded && !own->overwritten && (own->uploaded || own->pub.gpu_owned);
}
static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view) {
*view = s->v;
*has_view = s->has_view;
@@ -4117,9 +4132,11 @@ static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view) {
if (!any && (!cpu_slot || alias_has_gpu_content(p))) any = p;
}
// A finalized CPU descriptor must not recreate its image from freed storage. The slot keeps this state even
// after its released image was evicted; registering the descriptor again makes it current.
// after its released image was evicted; registering the descriptor again makes it current. While its own
// entry still holds the texture (stale_kept), the slot samples that image as it is.
if (!e && any) e = any;
else if (!e && !cpu_slot) e = own && own->superseded ? NULL : own; // a superseded one: entry_of takes it back
else if (!e && cpu_slot && s->finalized && stale_kept(own)) e = own, n_stale_kept++;
else if (!e && cpu_slot && s->finalized) stale = 1, n_stale++, n_stale_hm += terrain_heightmap(&s->t);
}
resolve_stale = stale;
@@ -4263,7 +4280,8 @@ const VkTexImage *vk_texture_by_id_locked(int id, view_t *view, int *has_view) {
// held slot counts as checked for look_round: the textures evicted by disuse are registered slots all the same.
if ((e || s->cheld) && s->cepoch != submit_epoch) {
round_checks++;
if (e && (!e->uploaded || e->checked != submit_epoch)) {
// a released entry here is a finalized slot's own (stale_kept): the storage may be the game's to free or reuse
if (e && !e->released && (!e->uploaded || e->checked != submit_epoch)) {
make_fresh_snap(e);
e->checked = submit_epoch;
touch(e);
@@ -4380,10 +4398,10 @@ void vk_texture_new_submit(void) {
n_mark_empty = n_mark_none = n_drop_wb = 0;
rt_log("perf: frame %llu: texture finalizes: slots marked %llu; slots of another live texture with the same "
"parameters at the storage, left current %llu (terrain heightmaps %llu); lookups the stale rule gave the "
"stand-in %llu (terrain heightmaps %llu)", (unsigned long long)f * 60, (unsigned long long)n_fin_own,
(unsigned long long)n_fin_other, (unsigned long long)n_fin_other_hm, (unsigned long long)n_stale,
(unsigned long long)n_stale_hm);
n_fin_own = n_fin_other = n_fin_other_hm = n_stale = n_stale_hm = 0;
"stand-in %llu (terrain heightmaps %llu), their own kept image %llu (NX2NAVI_TEX_STALE_KEPT)",
(unsigned long long)f * 60, (unsigned long long)n_fin_own, (unsigned long long)n_fin_other, (unsigned long long)n_fin_other_hm,
(unsigned long long)n_stale, (unsigned long long)n_stale_hm, (unsigned long long)n_stale_kept);
n_fin_own = n_fin_other = n_fin_other_hm = n_stale = n_stale_hm = n_stale_kept = 0;
if (split_mode > 0)
rt_log("perf: frame %llu: split fill: entries the CPU does not write started with their storage's bytes: "
"copied from the released entry they replaced %llu, filled from storage %llu (%.1f MiB, %.2f ms), "