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);