From 81e7da91aac417cc903c3bde304b181f606cd87a Mon Sep 17 00:00:00 2001 From: chrg6 Date: Thu, 12 Nov 2026 10:46:01 -0500 Subject: [PATCH] Bring --probe-vkmem's texture cases up to the rules the runtime has now. The probe ended in FAIL on an unchanged tree, so none of its cases after the texture slots ran. Three of its expectations were older than the rule they test; the runtime does what its comments say in each. A finalized slot of a CPU-written texture shows its own image while that image still holds the texture (stale_kept). The probe expected such a slot to resolve to nothing after its texture's release although another texture object kept the image current. It now expects the image with tex_stale_kept on and nothing with it off, and a case of its own covers the rule: after the finalize the slot shows the same image, bytes written to the storage afterwards do not reach it and nothing is uploaded; registered again, in a later submission, it holds the storage's bytes of then. A slot follows another descriptor's live texture at its storage only when that texture has the slot's size (follow_size_ok). The probe put a 32x32 texture under a 64x64 slot and expected the slot to follow it. It now checks both: a 64x64 texture of another format is followed, and under a 32x32 one the slot gets an image of its own descriptor. The texture detail cases and the case of an ASTC decode without scratch memory returned without a check whenever the image was neither RGBA8 nor BC3, which was meant for a device that samples ASTC itself. Since opaque ASTC textures are made as BC1 that held for every image of those cases: the probe printed their line and checked nothing. They read BC1 images now. The probe passes with the default settings (188 GPU checks; 164 when it last passed), with tex_stale_kept = 0, tex_same_big = 1, pool_presnap_runs = 0, shadow storage, ASTC images as BC3 and as RGBA8, and texture detail reduced and low. Co-Authored-By: Claude Fable 5.2 --- runtime/tex_probe.c | 110 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 12 deletions(-) diff --git a/runtime/tex_probe.c b/runtime/tex_probe.c index 3905c530..922ce06e 100644 --- a/runtime/tex_probe.c +++ b/runtime/tex_probe.c @@ -764,11 +764,19 @@ static int detail_fill(const tex_t *t, unsigned layers, unsigned round) { return 0; } -// Whether level lv, layer ly of image (RGBA8 or BC3, w x h at that level) is `want` in every texel. +// The image formats of an ASTC texture whose levels detail_level_is can read: the RGBA8 decode, its BC3 encoding, and +// the BC1 one an opaque texture gets (vk_texture.c astc_reformat; these cases' blocks are opaque). An image in the ASTC +// format itself, where the device samples it, is not read here. +static int detail_readable(VkFormat f) { + return f == VK_FORMAT_R8G8B8A8_UNORM || f == VK_FORMAT_BC3_UNORM_BLOCK || f == VK_FORMAT_BC1_RGB_UNORM_BLOCK; +} + +// Whether level lv, layer ly of image (RGBA8, BC3 or BC1, w x h at that level) is `want` in every texel. static int detail_level_is(const VkTexImage *img, uint32_t lv, uint32_t ly, uint32_t w, uint32_t h, const uint8_t want[4]) { VkCtx *c = vk_ctx(); int bc = img->format == VK_FORMAT_BC3_UNORM_BLOCK || img->format == VK_FORMAT_BC3_SRGB_BLOCK; - size_t n = bc ? (size_t)((w + 3) / 4) * ((h + 3) / 4) * 16 : (size_t)w * h * 4; + int bc1 = img->format == VK_FORMAT_BC1_RGB_UNORM_BLOCK || img->format == VK_FORMAT_BC1_RGB_SRGB_BLOCK; + size_t n = bc || bc1 ? (size_t)((w + 3) / 4) * ((h + 3) / 4) * (bc ? 16 : 8) : (size_t)w * h * 4; VkBuffer rb; VmaAllocation ra; uint8_t *r = vk_staging(n, &rb, &ra); @@ -795,6 +803,15 @@ static int detail_level_is(const VkTexImage *img, uint32_t lv, uint32_t ly, uint memcpy(&e0, r + i + 8, 2), memcpy(&e1, r + i + 10, 2); same &= e0 == e && e1 == e; } + } else if (bc1) { // BC1: the endpoints at bytes 0 to 3, then 2 bits a texel. With equal endpoints the selectors 0 + // to 2 all give the colour; 3 is black in this format, so none may be 3. + uint16_t e = (uint16_t)((want[0] >> 3) << 11 | (want[1] >> 2) << 5 | want[2] >> 3); + for (size_t i = 0; i < n; i += 8) { + uint16_t e0, e1; + uint32_t sel; + memcpy(&e0, r + i, 2), memcpy(&e1, r + i + 2, 2), memcpy(&sel, r + i + 4, 4); + same &= e0 == e && e1 == e && !(sel & sel >> 1 & 0x55555555u); + } } else { for (size_t i = 0; i < n; i += 4) same &= !memcmp(r + i, want, 4); } @@ -817,7 +834,7 @@ static int detail_case(pool_t *pool, uint64_t off, uint64_t room, uint32_t size, if (detail_fill(&t, layers, round)) return 1; const VkTexImage *img = vk_texture_get(&t); GCHECK(img && img->mirrored, "ASTC %ux%u texture image", size, size); - if (img->format != VK_FORMAT_R8G8B8A8_UNORM && img->format != VK_FORMAT_BC3_UNORM_BLOCK) return 0; // native ASTC + if (!detail_readable(img->format)) return 0; // native ASTC GCHECK(img->skip == want_skip && img->levels == levels - want_skip && img->width == size >> want_skip && img->height == size >> want_skip && img->layers == layers, "ASTC %ux%u, %u levels, %u layers: image %ux%u with %u levels, %u layers, made without %u (want %u)", size, size, @@ -855,7 +872,7 @@ static int detail_retry(pool_t *pool, uint64_t off, uint64_t room) { const VkTexImage *img = vk_texture_get(&t); vk_astc_scratch_fail(0); GCHECK(img && img->mirrored, "the retry case's texture image"); - if (img->format != VK_FORMAT_R8G8B8A8_UNORM && img->format != VK_FORMAT_BC3_UNORM_BLOCK) return 0; // native ASTC + if (!detail_readable(img->format)) return 0; // native ASTC vk_texture_debug(img, &ups, &ch, &pw); GCHECK(vk_texture_upload_retries() == r0 + 1 && ups == 1, "an ASTC upload whose decode had no memory was not left for another try (%llu tries noted, %u uploads)", @@ -968,8 +985,15 @@ static int test_textures(pool_t *tpool, pool_t *phys) { vk_texture_register(6, &bn, NULL); vk_texture_release(&b); const VkTexImage *kept = vk_texture_by_id(6, NULL, NULL); - GCHECK(kept && kept->width == 64 && kept->height == 48 && !vk_texture_by_id(5, NULL, NULL), - "a release finalized slot 6 of another live texture, or left its own slot 5 current"); + GCHECK(kept && kept->width == 64 && kept->height == 48, "a release finalized slot 6 of another live texture"); + // Its own slot 5 is finalized. The texture's image still holds the texture (slot 6's texture keeps it current), so + // with the kept-stale rule the slot shows that image (test_stale_kept, vk_texture.c stale_kept); without the rule + // it resolves to nothing. + const VkTexImage *own5 = vk_texture_by_id(5, NULL, NULL); + if (rt_knob("TEX_STALE_KEPT", "1") == 1) + GCHECK(own5 && own5->image == kept->image, "finalized slot 5 does not show its texture's image, which is still current"); + else + GCHECK(!own5, "a release left its own slot 5 current"); vk_texture_release(&bn); // 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 @@ -987,14 +1011,26 @@ static int test_textures(pool_t *tpool, pool_t *phys) { 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. + // GPU-owned slots do follow the live image at their storage, even when its descriptor has changed, but only one of + // their own size (vk_texture.c follow_size_ok): the game's render targets share storage, and the live one at an + // address is often a target of another size that a frame laid out there. The 64x64 slot gets the image of a 64x64 + // texture of another format; once that one is released and a 32x32 one is live there, the slot gets an image of + // its own descriptor. (In the other order the slot's own entry is live again after the second step, and a slot + // whose own entry is live keeps it.) vk_texture_register(5, &g, NULL); vk_texture_release(&g); - tex_t g2 = make_tex(phys, 0, NVN_FORMAT_R32UI, 32, 32, 1, 5); + tex_t g2 = make_tex(phys, 0, NVN_FORMAT_R32UI, 64, 64, 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(live && slot && slot->image == live_img, "slot 5 does not follow the live texture of its size at its storage"); + vk_texture_release(&g2); + tex_t g3 = make_tex(phys, 0, NVN_FORMAT_R32UI, 32, 32, 1, 8); + live = vk_texture_get(&g3); + slot = vk_texture_by_id(5, NULL, NULL); + GCHECK(live && slot && slot->image != live->image && slot->width == 64 && slot->height == 64 && + slot->format == VK_FORMAT_R8G8B8A8_UNORM, "64x64 slot 5 follows the 32x32 texture at its storage, or has no " + "image of its own descriptor"); 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; @@ -1002,8 +1038,8 @@ 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 += 12; - vk_texture_release(&t), vk_texture_release(&b2), vk_texture_release(&g2); + g_gpu_checks += 14; + vk_texture_release(&t), vk_texture_release(&b2), vk_texture_release(&g2), vk_texture_release(&g3); for (int i = 0; i < 4; i++) free(lin[i]), free(bl[i]); free(gl[0]); uint64_t images, ups, reups; @@ -1041,6 +1077,56 @@ static void image_bytes(VkImage image, uint32_t w, uint32_t h, uint8_t *out) { vmaDestroyBuffer(c->vma, rb, ra); } +// The kept-stale rule (vk_texture.c stale_kept, slot_resolve; NX2NAVI_TEX_STALE_KEPT, default on). The slot of a +// texture the CPU writes, finalized with its texture, goes on showing that texture's own image while the image still +// holds the texture: the game finalizes ahead of the replay of the frames it recorded before. The image is sampled as +// it is, so bytes the storage gets after the finalize do not reach it and nothing is uploaded. With the switch off the +// slot resolves to nothing, which a draw shows as the stand-in. Either way a registration makes the descriptor current +// again, with the storage's bytes as they are then. That another descriptor made at the storage ends the rule is the +// "stale CPU slot" case of test_textures. +static int test_stale_kept(pool_t *tpool) { + int on = rt_knob("TEX_STALE_KEPT", "1") == 1; + uint8_t *ref[1] = {0}, *ref2[1] = {0}, got[16 * 16 * 4]; + tex_t k = make_tex(tpool, 0xc0000, 0x25, 16, 16, 1, 60); + if (fill_storage(&k, ref)) return 1; + vk_texture_register(9, &k, NULL); + const VkTexImage *own = vk_texture_by_id(9, NULL, NULL); + GCHECK(own && own->image && own->width == 16 && own->height == 16, "slot 9 does not resolve its registered texture"); + VkImage own_img = own->image; + image_bytes(own_img, 16, 16, got); + GCHECK(!memcmp(got, ref[0], sizeof got), "the image of slot 9 does not hold the storage's bytes"); + uint64_t up0, re0, up1, re1; + vk_texture_stats(NULL, &up0, &re0); + vk_texture_release(&k); + if (fill_storage(&k, ref2)) return 1; // what the game puts into the storage next + const VkTexImage *after = vk_texture_by_id(9, NULL, NULL); + vk_texture_stats(NULL, &up1, &re1); + if (on) { + GCHECK(after && after->image == own_img, "a finalized CPU slot does not show its texture's own image"); + image_bytes(own_img, 16, 16, got); + GCHECK(!memcmp(got, ref[0], sizeof got) && up1 == up0 && re1 == re0, "the kept image of a finalized slot took the " + "storage's later bytes (%llu uploads and %llu re-uploads since the finalize)", + (unsigned long long)(up1 - up0), (unsigned long long)(re1 - re0)); + } else { + GCHECK(!after && up1 == up0 && re1 == re0, "a finalized CPU slot resolves with NX2NAVI_TEX_STALE_KEPT=0"); + } + // In a later submission: an entry's storage is checked for writes once per submission (vk_texture_by_id_locked), + // and this one's was at the first lookup above. + vk_texture_new_submit(); + vk_texture_register(9, &k, NULL); + const VkTexImage *again = vk_texture_by_id(9, NULL, NULL); + GCHECK(again && again->image && again->width == 16 && again->height == 16, "slot 9 registered again does not resolve"); + image_bytes(again->image, 16, 16, got); + GCHECK(!memcmp(got, ref2[0], sizeof got), "slot 9 registered again does not hold the bytes the storage has now"); + vk_texture_release(&k); + free(ref[0]), free(ref2[0]); + printf("probe-vkmem: a finalized slot of a CPU-written texture: %s (NX2NAVI_TEX_STALE_KEPT=%d); registered again it " + "holds the storage's bytes of then\n", + on ? "its own image as it was, whatever the storage gets afterwards" : "nothing", on); + g_gpu_checks += on ? 6 : 5; + return 0; +} + // ---- two descriptors on one CPU-written storage (vk_texture.c "split fill", start_at) // D1 is a texture the CPU writes. D2 names the same storage with a texture flag for which the cache does not take the // content from the CPU (COMPRESSIBLE here), so a slot of D2 shows D1's image for as long as D1 has a live entry. @@ -1186,7 +1272,7 @@ static int test_gpu(void) { .size = 0x80000, .span = 0x80000}; if (test_import(&pool) || test_readfile(pool.mem + 0x38000) || test_virtual(&phys, &virt)) return 1; GCHECK(vk_pool_init(&tpool), "vk_pool_init of the texture pool"); - if (test_textures(&tpool, &phys)) return 1; + if (test_textures(&tpool, &phys) || test_stale_kept(&tpool)) return 1; pool_t dpool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = h + 0x500000, .size = 0x100000, .game_mem = h + 0x500000}; GCHECK(vk_pool_init(&dpool), "vk_pool_init of the texture detail pool"); int detail_bad = test_detail(&dpool);