mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Keep a texture's finalization on its registered slots, so a stale slot stays stale after its image was evicted.
Whether a slot was stale was read from the released image's entry, which eviction, the released cap or a new image at the address can destroy before the slot is looked up again; the slot then revived the texture from storage the game had freed (the access violation in the block copy during a pool refresh). The flag now lives on the slot: a release sets it on the slots registered with that descriptor, a registration clears it, and a finalized slot of a CPU-written texture follows only live GPU content at its address. From pull request #348 by TophC7, taken as it is. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,9 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
|
||||
(`GfxShaderRes.ptr`, `vk_exec_draw.c ptr_addr`).
|
||||
- Bindless textures: the texture and sampler pools are descriptor arrays kept in step with the game's pool writes
|
||||
(`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.
|
||||
- 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
|
||||
|
||||
@@ -699,14 +699,21 @@ static int test_textures(pool_t *tpool, pool_t *phys) {
|
||||
vk_write_texels(&g, NULL, &all, gl[0], 256, 256 * 64);
|
||||
if (read_back(&g, 0, gl, 0, "physical-pool RGBA8 WriteTexels")) return 1;
|
||||
|
||||
// a slot of a finalized texture follows the live texture at the same storage. The slot's own storage is CPU-written
|
||||
// (mirrored), so it follows only a texture holding GPU content its bytes lack (vk_texture.c slot_resolve). The live
|
||||
// texture is cleared on the GPU and noted as rendered into, as a draw into it would be; the slot's image must then
|
||||
// show the clear, which neither the storage bytes nor a recreated image of the slot's own descriptor hold.
|
||||
// A finalized CPU-backed slot must not revive its stale bytes or follow a new, unrendered CPU texture.
|
||||
vk_texture_register(5, &b, NULL);
|
||||
vk_texture_release(&b);
|
||||
tex_t b2 = make_tex(tpool, 0x40000, 0x25, 16, 12, 1, 4);
|
||||
const VkTexImage *live = vk_texture_get(&b2);
|
||||
GCHECK(live && !vk_texture_by_id(5, NULL, NULL), "stale CPU slot 5 revived or followed unrendered storage");
|
||||
// Registration is the lifetime boundary that makes the descriptor current again, even after its image was evicted.
|
||||
vk_texture_register(5, &b, NULL);
|
||||
const VkTexImage *renewed = vk_texture_by_id(5, NULL, NULL);
|
||||
GCHECK(renewed && renewed->format == VK_FORMAT_BC1_RGBA_UNORM_BLOCK && renewed->width == 64 && renewed->height == 48,
|
||||
"re-registered CPU slot 5 did not resolve its own descriptor");
|
||||
vk_texture_release(&b);
|
||||
// Once the live texture at its storage holds GPU content the storage bytes lack, the finalized CPU slot follows it.
|
||||
// The live texture is cleared on the GPU and noted as rendered into, as a draw into it would be; the slot's image
|
||||
// must then show the clear, which neither the storage bytes nor a recreated image of the slot's own descriptor hold.
|
||||
GCHECK(live && live->image && live->width == 16 && live->height == 12, "RGBA8 image of the texture at the slot's storage");
|
||||
vk_texture_gpu_written(&b2); // the clear below is its content, not the storage
|
||||
const float clear[4] = {0.0f, 1.0f, 1.0f, 1.0f};
|
||||
@@ -717,10 +724,17 @@ static int test_textures(pool_t *tpool, pool_t *phys) {
|
||||
VkCommandBuffer ccb = vk_upload_begin();
|
||||
vkCmdClearColorImage(ccb, live->image, VK_IMAGE_LAYOUT_GENERAL, &cv, 1, &rng);
|
||||
vk_upload_end();
|
||||
const VkTexImage *followed = vk_texture_by_id(5, NULL, NULL);
|
||||
GCHECK(followed && followed->image == live->image, "finalized CPU slot 5 does not follow the rendered texture at its storage");
|
||||
GCHECK(image_is_color(followed->image, 16, 12, cleared), "the image of slot 5 does not hold the clear written into the live texture");
|
||||
// GPU-owned slots do follow the live image at their storage, even when its descriptor has changed.
|
||||
vk_texture_register(5, &g, NULL);
|
||||
vk_texture_release(&g);
|
||||
tex_t g2 = make_tex(phys, 0, NVN_FORMAT_R32UI, 32, 32, 1, 5);
|
||||
live = vk_texture_get(&g2);
|
||||
VkImage live_img = live ? live->image : VK_NULL_HANDLE;
|
||||
const VkTexImage *slot = vk_texture_by_id(5, NULL, NULL);
|
||||
GCHECK(live && slot && slot->image == live_img, "slot 5 does not follow the live texture at its storage");
|
||||
GCHECK(image_is_color(slot->image, 16, 12, cleared), "the image of slot 5 does not hold the clear written into the live texture");
|
||||
samp_par sp;
|
||||
memset(&sp, 0, sizeof sp);
|
||||
sp.minf = NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR, sp.magf = NVN_MAG_FILTER_LINEAR, sp.ws = sp.wt = sp.wr = NVN_WRAP_MODE_REPEAT;
|
||||
@@ -728,14 +742,15 @@ static int test_textures(pool_t *tpool, pool_t *phys) {
|
||||
vk_sampler_register(0, &sp);
|
||||
VkSampler s0 = vk_sampler_by_id(0), s1 = vk_sampler_for(&sp);
|
||||
GCHECK(s0 && s0 == s1 && !vk_sampler_by_id(1), "sampler slots");
|
||||
g_gpu_checks += 9;
|
||||
vk_texture_release(&t), vk_texture_release(&b2), vk_texture_release(&g);
|
||||
g_gpu_checks += 12;
|
||||
vk_texture_release(&t), vk_texture_release(&b2), vk_texture_release(&g2);
|
||||
for (int i = 0; i < 4; i++) free(lin[i]), free(bl[i]);
|
||||
free(gl[0]);
|
||||
uint64_t images, ups, reups;
|
||||
vk_texture_stats(&images, &ups, &reups);
|
||||
printf("probe-vkmem: textures: RGBA8 3 levels and BC1 equal to the CPU reference after upload, re-upload after CPU "
|
||||
"writes, physical-pool alias WriteTexels/ReadTexels, slot redirect, samplers (%llu images, %llu uploads, "
|
||||
"writes, physical-pool alias WriteTexels/ReadTexels, stale CPU slot, CPU slot following rendered content, GPU slot "
|
||||
"redirect, samplers (%llu images, %llu uploads, "
|
||||
"%llu re-uploads)\n", (unsigned long long)images, (unsigned long long)ups, (unsigned long long)reups);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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 slot of a finalized texture follows the live texture at its storage address.
|
||||
// "GPU-owned" rule); a finalized slot follows live GPU content 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
|
||||
@@ -56,7 +56,6 @@ typedef struct Tex {
|
||||
uint64_t retire_takes; // vk_pool_takes when it was retired
|
||||
uint64_t retire_due; // vk_sync_issued once every pool set was taken since (0: not yet); destroyed after it
|
||||
int uploaded, released;
|
||||
uint32_t rel_serial; // reg_serial when it was released: slots registered before then are stale (slot_resolve)
|
||||
uint64_t uid; // names the entry in its bucket's released set (tex_gen_flip); 0 until its first release
|
||||
uint64_t rel_frame; // gfx_counters.frames when it was released (drop_stale)
|
||||
uint32_t back_gap; // most presents between a release and its taking back so far (stale_after)
|
||||
@@ -101,7 +100,7 @@ typedef struct {
|
||||
uint32_t cgen; // agen of the slot's address when ce was resolved
|
||||
uint32_t cepoch;
|
||||
view_t cview;
|
||||
uint8_t chas_view, used, has_view;
|
||||
uint8_t chas_view, used, has_view, finalized; // finalized survives eviction of the released Tex entry
|
||||
uint8_t cheld; // ce is NULL as the texture was evicted by disuse, valid while cgen is (held_out)
|
||||
uint8_t cwant; // a shader sampled its stand-in (vk_pool_marks_read): slot_resolve makes it at once
|
||||
uint64_t khash; // hash of the descriptor key and view, 0 for CPU-written textures (vk_texture_slot_khash_locked)
|
||||
@@ -164,7 +163,7 @@ static int nowned, cowned;
|
||||
static uint64_t max_owned; // largest storage size in owned[]
|
||||
static uint64_t gpu_writes; // GPU writes into mirrored textures so far: Tex.gpu_seq
|
||||
// Bumped whenever the entry a slot resolves to can change: an entry created or destroyed, released or taken back into
|
||||
// use (the slot lookup and the follow of a finalized slot read only by_key, by_addr and the released flags).
|
||||
// use, or a registered slot finalized (the follow reads by_key, by_addr and the entry and slot lifetime flags).
|
||||
static uint32_t tex_gen = 1;
|
||||
// Bumped with tex_gen and whenever a slot is registered: what vk_texture_by_id returns for any slot may have changed.
|
||||
static uint32_t slots_gen = 1;
|
||||
@@ -257,10 +256,10 @@ static void tex_gen_bump(uint64_t addr) {
|
||||
// generation never stands for two states. The global generations still move, so their readers look, and find the
|
||||
// tokens equal. plain: the take-back of an entry that was superseded or overwritten while released changes more than
|
||||
// the flag, and is a change like any other.
|
||||
// One input of slot_resolve is not in the state: the stale rule compares a slot's registration with the entry's
|
||||
// release (rel_serial), which is later at every release. A slot is stale at every later release once it is at one, so
|
||||
// what it resolves to in a released state stays right; the one that is not stale takes the entry back, which is the
|
||||
// lookup-made change in_lookup deals with.
|
||||
// One input of slot_resolve is not in the state: the stale rule reads the slot's finalized flag, which a release sets
|
||||
// on the slots registered with its descriptor and a registration clears. A slot is stale at every later release once
|
||||
// it is at one, so what it resolves to in a released state stays right; the one that is not stale takes the entry
|
||||
// back, which is the lookup-made change in_lookup deals with.
|
||||
static void tex_gen_flip(Tex *e, int plain) {
|
||||
static int on = -1;
|
||||
static uint64_t uids;
|
||||
@@ -2329,7 +2328,6 @@ static int wb_wanted(const Tex *e) { return e->t.L.size <= wb_limit || e->run <
|
||||
static void mark_released(Tex *e) { // tex_lock held; e is not released
|
||||
e->released = 1;
|
||||
e->rel_epoch = submit_epoch;
|
||||
e->rel_serial = reg_serial;
|
||||
e->rel_frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
nreleased++;
|
||||
rel_append(e);
|
||||
@@ -2399,10 +2397,24 @@ void vk_texture_release(const tex_t *t) {
|
||||
make_key(t, &k);
|
||||
lock();
|
||||
Tex *e = nvn_hm_get(&by_key, &k);
|
||||
// Staleness belongs to the registered descriptor, not the cached image: a released Tex can be destroyed before the
|
||||
// slot looks again (eviction, the released cap, start_at for a mirrored one). A registered texture may also be
|
||||
// finalized before its image is created.
|
||||
int changed = 0;
|
||||
if (sinfo)
|
||||
for (int id = bucket_head[agen_idx(k.addr)]; id >= 0; id = slot_next[id]) {
|
||||
TexSlot *s = &tslots[id];
|
||||
if (s->finalized || (uintptr_t)s->t.mem != k.addr) continue;
|
||||
TexKey sk;
|
||||
make_key(&s->t, &sk);
|
||||
if (!memcmp(&sk, &k, sizeof k)) s->finalized = 1, s->ce = NULL, changed = 1;
|
||||
}
|
||||
if (e && !e->released) {
|
||||
mark_released(e);
|
||||
changed = 0; // the image's release already invalidated the address bucket
|
||||
sweep_released();
|
||||
}
|
||||
if (changed) tex_gen_bump(k.addr);
|
||||
unlock();
|
||||
}
|
||||
|
||||
@@ -2447,7 +2459,7 @@ void vk_texture_register(int id, const tex_t *t, const view_t *v) {
|
||||
// again (slot_resolve's stale rule), so it goes the full way. A slot held out of the pool (cheld) keeps that too:
|
||||
// the same descriptor names the same held texture. Both only while the resolution is current (cgen): a destroyed
|
||||
// entry bumped the generation and its record may be freed, so ce is not read past that.
|
||||
int current = s->used && s->cgen == agen[sinfo[id] & 0xffff];
|
||||
int current = s->used && !s->finalized && s->cgen == agen[sinfo[id] & 0xffff];
|
||||
if (current && ((s->ce && !s->ce->released) || s->cheld) && !memcmp(&s->t, t, sizeof *t) &&
|
||||
s->has_view == (v != NULL) && (!v || !memcmp(&s->v, v, sizeof *v))) {
|
||||
n_same_regs++;
|
||||
@@ -2457,6 +2469,7 @@ void vk_texture_register(int id, const tex_t *t, const view_t *v) {
|
||||
s->t = *t;
|
||||
if (!s->used && id < VK_POOL_TEX) pool_slots++;
|
||||
s->used = 1;
|
||||
s->finalized = 0;
|
||||
s->ce = NULL;
|
||||
s->cheld = 0;
|
||||
slots_gen++;
|
||||
@@ -2515,7 +2528,7 @@ static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view) {
|
||||
make_key(&s->t, &k);
|
||||
Tex *e = nvn_hm_get(&by_key, &k);
|
||||
int stale = 0;
|
||||
if (!e || e->released) { // a finalized slot follows the live texture at its storage
|
||||
if (!e || e->released || s->finalized) { // a finalized slot follows live GPU content, not newly reused CPU bytes
|
||||
// Prefer the live entry that differs from the slot descriptor only in its swizzle: transient render targets
|
||||
// share storage, and the newest one at the address is usually another size. The map screen samples its
|
||||
// 2250x1080 map target through an (R,G,B,1) descriptor and followed a 960x60 target at the same address.
|
||||
@@ -2535,17 +2548,14 @@ static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view) {
|
||||
// that 2D image, no 3D view can be made of it (vk_exec_draw.c get_view) and they read and wrote stand-ins.
|
||||
// With the target kept they get the volume itself.
|
||||
if (p->key.d.target != k.d.target) continue;
|
||||
if (same_but_swizzle(&p->key, &k)) { e = p; break; }
|
||||
if (same_but_swizzle(&p->key, &k) && (!cpu_slot || !s->finalized || alias_has_gpu_content(p))) { e = p; break; }
|
||||
if (!any && (!cpu_slot || alias_has_gpu_content(p))) any = p;
|
||||
}
|
||||
// own is missing or released here; for a CPU slot entry_of below finds or revives it by key. A CPU slot whose
|
||||
// own entry was released after the slot was registered is stale: the game finalized that texture and is
|
||||
// freeing its storage, and reviving it uploaded from that storage while another thread unmapped it (an
|
||||
// access violation in tex_bl_copy from the pool refresh, Kokiri Forest). It resolves to nothing; a slot
|
||||
// registered again after the release is live and revives the entry.
|
||||
// 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.
|
||||
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 && own && (uint32_t)(sinfo[s - tslots] >> 16) <= own->rel_serial) stale = 1;
|
||||
else if (!e && cpu_slot && s->finalized) stale = 1;
|
||||
}
|
||||
if (!e && s->t.mem && !stale) {
|
||||
// evicted by disuse: the pool refresh keeps the stand-in, without spending the creation budget on it
|
||||
|
||||
Reference in New Issue
Block a user