diff --git a/docs/architecture.md b/docs/architecture.md index a2d98cd4..503aafe4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/runtime/tex_probe.c b/runtime/tex_probe.c index f7c64eb6..b57e7bdb 100644 --- a/runtime/tex_probe.c +++ b/runtime/tex_probe.c @@ -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; } diff --git a/runtime/vk_texture.c b/runtime/vk_texture.c index 1ea5661b..e3a58b85 100644 --- a/runtime/vk_texture.c +++ b/runtime/vk_texture.c @@ -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