diff --git a/docs/building.md b/docs/building.md index 7126091c..04dbf920 100644 --- a/docs/building.md +++ b/docs/building.md @@ -259,6 +259,7 @@ change passes these, all run with `.venv`: | `tests\diff\func.py` | whole functions as the generator emits them, loops, jump tables and calls included (calls go to a stub that checks the registers at the call) | yes | | `tests\flags_check.py` | the function-local flag helpers of `runtime\cpu.h` against its packed ones | no | | `tests\ds3_check.py` | the DualShock 3 report decoding and output report of `runtime\input_ds3.c` | no | +| `tests\gpa_check.py` | the device address table of `runtime\vk_gpa.c` as a shader reads it: mapping, mirrors and twins, overlapping imports, and blocks given back and taken again while pool addresses climb (no GPU; any Python) | no | | `tests\excl_mt.py` | the exclusive monitor helpers under contention and ABA, on several threads | no | | `tests\jumptables_check.py` | `recomp/jumptables.py` against a Unicorn oracle; must report 0 disagreements, on 1.4.3 too | yes | diff --git a/runtime/vk_gpa.c b/runtime/vk_gpa.c index d313ebe3..41bc8253 100644 --- a/runtime/vk_gpa.c +++ b/runtime/vk_gpa.c @@ -25,6 +25,14 @@ // the new one, never half of each. The CPU keeps a copy (shadow) for its reads: the GPU copy is often device memory // behind the PCIe BAR, slow to read back. // +// An L2 block whose last page is unmapped leaves its L1 entry at once (blocks_release) and serves another 16 MiB range +// once the GPU finished every submission issued while a shader could still have read the old L1 entry: until then it +// holds only zeros, which read as unmapped. The game's allocator hands out pool addresses that climb through the 64 GiB +// alias region, so blocks kept for good ran out after 16 GiB of ranges ever mapped (39 minutes of play, six areas), and +// every pool created after that stayed out of the table: its pointers read zeros, as before the table existed. A range +// mapped again while its block still waits takes that block back (block_take), so a pool freed and made again at its +// address within a frame holds no second block. +// // NX2NAVI_GPA=0 turns the table off: the translator is told the host has no device addresses (those accesses read 0). // NX2NAVI_GPA_EMPTY=1 keeps it empty, so every access through it reads the zero buffer (diagnosis). // Mirrored pools are not synced for the table's readers (vk_exec_draw.c ssbo_info says why). @@ -54,7 +62,14 @@ static struct { VmaAllocation alloc, dummy_alloc, scratch_alloc; volatile uint32_t *gpu; // the mapped table uint32_t *cpu; // its shadow - uint32_t nblocks; // L2 blocks handed out (block 0 is the zero block) + uint32_t nblocks; // L2 blocks ever handed out (block 0 is the zero block) + uint16_t blk_pages[BLOCKS]; // pages each block maps + uint16_t blk_l1[BLOCKS]; // the L1 entry that names it, or named it while it waits + uint8_t blk_wait[BLOCKS]; // off L1, waiting for the GPU (blocks_release) + uint32_t blk_gen[BLOCKS]; // releases so far: a block taken back and released again has two callbacks + uint16_t free_blks[BLOCKS]; // empty blocks whose fence completed + uint32_t nfree_blk, blk_waiting; + uint64_t blk_returned; // blocks taken off L1 so far uint32_t nrec; // records handed out, free ones aside uint32_t *free_recs, nfree; // records whose fence completed uint64_t mapped_pages, maps, unmaps, refused; @@ -133,19 +148,78 @@ VkDescriptorBufferInfo vk_gpa_info(void) { return (VkDescriptorBufferInfo){G.buf, 0, VK_WHOLE_SIZE}; } +// A block for L1 entry l1, 0 when none is left: the one the entry gave up and that still waits for the GPU (it maps +// nothing, and a shader that read the old entry finds the new pages in it), else a free one, else one never used. +static uint32_t block_take(uint32_t l1) { + uint32_t blk = 0; + for (uint32_t b = 1; G.blk_waiting && b < G.nblocks && !blk; b++) + if (G.blk_wait[b] && G.blk_l1[b] == l1) blk = b; + if (blk) G.blk_wait[blk] = 0, G.blk_waiting--; // its callback finds it in use (blk_free_cb) + else if (G.nfree_blk) blk = G.free_blks[--G.nfree_blk]; + else if (G.nblocks < BLOCKS) blk = G.nblocks++; + else return 0; + G.blk_l1[blk] = (uint16_t)l1; + return blk; +} + // The L2 entry word of a page; 0 when its block does not exist and alloc is 0 or no block is left. static uint32_t page_word(uint64_t page, int alloc) { uint64_t l1 = page >> 12; // 4096 pages of 4 KiB per 16 MiB if (l1 >= L1_N) return 0; uint32_t blk = G.cpu[L1_WORD + l1]; if (!blk) { - if (!alloc || G.nblocks == BLOCKS) return 0; - blk = G.nblocks++; + if (!alloc || !(blk = block_take((uint32_t)l1))) return 0; put(L1_WORD + (uint32_t)l1, blk); } return L2_WORD + blk * L2_N + (uint32_t)(page & (L2_N - 1)); } +// Stores record r (0: none) in the L2 entry word w and keeps the page counts of its block and of the table. +static void page_put(uint32_t w, uint32_t r) { + uint32_t blk = (w - L2_WORD) / L2_N, old = G.cpu[w]; + if (!old && r) G.blk_pages[blk]++, G.mapped_pages++; + else if (old && !r) G.blk_pages[blk]--, G.mapped_pages--; + put(w, r); +} + +static void blk_free_cb(void *arg) { + uint32_t blk = (uint32_t)((uintptr_t)arg & 0xffff), gen = (uint32_t)((uintptr_t)arg >> 16); + plat_mutex_lock(&gpa_lock); + if (G.state > 0 && G.blk_wait[blk] && G.blk_gen[blk] == gen) { // not taken back since (block_take) + G.blk_wait[blk] = 0, G.blk_waiting--; + G.free_blks[G.nfree_blk++] = (uint16_t)blk; + } + plat_mutex_unlock(&gpa_lock); +} + +// After pages of [p0, p1) were unmapped: the blocks of that range that map nothing leave L1, and go back to the free +// list once the GPU finished the submissions issued until now and the one being recorded, as a record does. +static void blocks_release(uint64_t p0, uint64_t p1) { + if (p1 <= p0) return; + for (uint64_t l1 = p0 >> 12; l1 <= (p1 - 1) >> 12 && l1 < L1_N; l1++) { + plat_mutex_lock(&gpa_lock); + uint32_t blk = G.cpu[L1_WORD + l1], gen = 0; + if (blk && !G.blk_pages[blk]) { + put(L1_WORD + (uint32_t)l1, 0); + G.blk_wait[blk] = 1, G.blk_waiting++, G.blk_returned++; + gen = ++G.blk_gen[blk]; + } else { + blk = 0; + } + plat_mutex_unlock(&gpa_lock); + if (blk) + vk_completion_add(vk_sync_issued() + 1, blk_free_cb, (void *)((uintptr_t)blk | (uintptr_t)gen << 16)); + } +} + +// gpa_lock held: a page found no block. Pointers into it read the zero buffer until its buffer is mapped again. +static void out_of_blocks(uint64_t page) { + if (!G.refused++) + rt_log("vk_gpa: out of L2 blocks at %#llx (%u in use, %u waiting for the GPU): shaders read zeros through " + "pointers into the pages left out", (unsigned long long)(page << PAGE_SHIFT), + G.nblocks - 1 - G.nfree_blk - G.blk_waiting, G.blk_waiting); +} + static uint32_t rec_alloc(uint64_t delta, uint64_t twin) { uint32_t r; if (G.nfree) r = G.free_recs[--G.nfree]; @@ -185,17 +259,16 @@ uint32_t vk_gpa_map(uint64_t lo, uint64_t hi, VkBuffer buf, uint64_t buf_lo) { if (G.refused == 1) rt_log("vk_gpa: out of records: a buffer at %#llx is not in the table", (unsigned long long)lo); return 0; } - uint64_t n = 0; for (uint64_t p = p0; p < p1; p++) { uint32_t w = page_word(p, 1); if (!w) { - if (!G.refused++) rt_log("vk_gpa: out of L2 blocks at %#llx", (unsigned long long)(p << PAGE_SHIFT)); - break; + out_of_blocks(p); + p |= L2_N - 1; // the rest of this 16 MiB has no block either; the next range may have one + continue; } - put(w, r); - n++; + page_put(w, r); } - G.mapped_pages += n, G.maps++; + G.maps++; plat_mutex_unlock(&gpa_lock); return r; } @@ -206,7 +279,7 @@ void vk_gpa_unmap(uint64_t lo, uint64_t hi, uint32_t rec, uint32_t (*cover)(uint plat_mutex_lock(&gpa_lock); for (uint64_t p = p0; p < p1; p++) { uint32_t w = page_word(p, 0); - if (w && G.cpu[w] == rec) put(w, 0), G.mapped_pages--; + if (w && G.cpu[w] == rec) page_put(w, 0); } G.unmaps++; plat_mutex_unlock(&gpa_lock); @@ -220,10 +293,11 @@ void vk_gpa_unmap(uint64_t lo, uint64_t hi, uint32_t rec, uint32_t (*cover)(uint uint32_t other = cover(p << PAGE_SHIFT); if (!other || other == rec) continue; plat_mutex_lock(&gpa_lock); - if (!G.cpu[w]) put(w, other), G.mapped_pages++; + if (!G.cpu[w]) page_put(w, other); plat_mutex_unlock(&gpa_lock); } } + blocks_release(p0, p1); // after the cover: a block another buffer's pages went back into stays vk_completion_add(vk_sync_issued() + 1, rec_free_cb, (void *)(uintptr_t)rec); } @@ -233,9 +307,10 @@ void vk_gpa_clear(uint64_t lo, uint64_t hi) { plat_mutex_lock(&gpa_lock); for (uint64_t p = p0; p < p1; p++) { uint32_t w = page_word(p, 0); - if (w && G.cpu[w]) put(w, 0), G.mapped_pages--; + if (w && G.cpu[w]) page_put(w, 0); } plat_mutex_unlock(&gpa_lock); + blocks_release(p0, p1); } static uint32_t remap(uint64_t lo, uint64_t hi, uint32_t old_rec, VkBuffer buf, uint64_t buf_lo, int keep_twin) { @@ -254,10 +329,12 @@ static uint32_t remap(uint64_t lo, uint64_t hi, uint32_t old_rec, VkBuffer buf, if (!r) { plat_mutex_unlock(&gpa_lock); return old_rec; } for (uint64_t p = p0; p < p1; p++) { uint32_t w = page_word(p, 1); - if (w && (!G.cpu[w] || G.cpu[w] == old_rec)) { - if (!G.cpu[w]) G.mapped_pages++; - put(w, r); + if (!w) { + out_of_blocks(p); + p |= L2_N - 1; + continue; } + if (!G.cpu[w] || G.cpu[w] == old_rec) page_put(w, r); } plat_mutex_unlock(&gpa_lock); if (old_rec) vk_completion_add(vk_sync_issued() + 1, rec_free_cb, (void *)(uintptr_t)old_rec); @@ -274,9 +351,14 @@ uint32_t vk_gpa_remap_plain(uint64_t lo, uint64_t hi, uint32_t old_rec, VkBuffer void vk_gpa_report(void) { if (G.state <= 0) return; - rt_log("vk_gpa: %llu pages (%.0f MiB) mapped, %u of %u blocks, %u records in use, %llu maps, %llu unmaps, %llu refused", - (unsigned long long)G.mapped_pages, G.mapped_pages * 4096.0 / 1048576.0, G.nblocks, BLOCKS, G.nrec - 1 - G.nfree, - (unsigned long long)G.maps, (unsigned long long)G.unmaps, (unsigned long long)G.refused); + plat_mutex_lock(&gpa_lock); + rt_log("vk_gpa: %llu pages (%.0f MiB) mapped, %u of %u blocks in use (%u ever, %u waiting for the GPU, %llu given " + "back), %u records in use, %llu maps, %llu unmaps, %llu refused", + (unsigned long long)G.mapped_pages, G.mapped_pages * 4096.0 / 1048576.0, + G.nblocks - 1 - G.nfree_blk - G.blk_waiting, BLOCKS - 1, G.nblocks - 1, G.blk_waiting, + (unsigned long long)G.blk_returned, G.nrec - 1 - G.nfree, (unsigned long long)G.maps, + (unsigned long long)G.unmaps, (unsigned long long)G.refused); + plat_mutex_unlock(&gpa_lock); } void vk_gpa_shutdown(void) { diff --git a/tests/gpa_check.c b/tests/gpa_check.c new file mode 100644 index 00000000..31e5c68f --- /dev/null +++ b/tests/gpa_check.c @@ -0,0 +1,233 @@ +// runtime/vk_gpa.c: the device address table against a model of what a shader reads through it, with the allocator, +// the device and the completion queue replaced by the stand-ins below. Built and run by tests/gpa_check.py; prints each +// failed check and exits 1 on any. +#include +#include +#include +#include + +#include "cfg.h" +#include "host.h" +#include "vk.h" +#include "vk_gpa.h" +#include "platform/platform.h" + +static int fails; +#define CHECK(c, ...) do { if (!(c) && fails++ < 40) { printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0) + +// ---- stand-ins for what vk_gpa.c links against +static VkCtx ctx = {.has_buffer_device_address = 1}; +VkCtx *vk_ctx(void) { return &ctx; } +const char *cfg_get(const char *name) { (void)name; return NULL; } +void plat_mutex_lock(PlatMutex *m) { (void)m; } +void plat_mutex_unlock(PlatMutex *m) { (void)m; } + +static int out_of_blocks_lines; +void rt_log(const char *fmt, ...) { + char line[512]; + va_list ap; + va_start(ap, fmt); + vsnprintf(line, sizeof line, fmt, ap); + va_end(ap); + if (strstr(line, "out of L2 blocks")) out_of_blocks_lines++; +} + +static const uint32_t *table; // the first buffer vk_gpa.c creates +static uintptr_t next_handle = 0x1000; +VkResult vmaCreateBuffer(VmaAllocator a, const VkBufferCreateInfo *bi, const VmaAllocationCreateInfo *ai, VkBuffer *buf, + VmaAllocation *alloc, VmaAllocationInfo *info) { + (void)a, (void)ai; + void *mem = calloc(1, (size_t)bi->size); + if (!table) table = mem; + *buf = (VkBuffer)(next_handle += 0x1000); + *alloc = (VmaAllocation)mem; + if (info) info->pMappedData = mem; + return VK_SUCCESS; +} +void vmaDestroyBuffer(VmaAllocator a, VkBuffer buf, VmaAllocation alloc) { (void)a, (void)buf; free(alloc); } + +// A buffer's device address: its handle far above the GPU addresses the tests map. +static uint64_t dev_of(VkBuffer b) { return (uint64_t)(uintptr_t)b << 28; } +VkDeviceAddress vkGetBufferDeviceAddress(VkDevice d, const VkBufferDeviceAddressInfo *i) { (void)d; return dev_of(i->buffer); } + +// The completion queue: a callback runs once the GPU is past its submission (gpu_done). +static struct { uint64_t fence; void (*fn)(void *); void *arg; } queue[1 << 16]; +static int nqueue; +static uint64_t issued; +uint64_t vk_sync_issued(void) { return issued; } +void vk_completion_add(uint64_t fence, void (*fn)(void *), void *arg) { + if (nqueue == (int)(sizeof queue / sizeof *queue)) { printf("FAIL: completion queue full\n"); exit(1); } + queue[nqueue].fence = fence, queue[nqueue].fn = fn, queue[nqueue].arg = arg; + nqueue++; +} +static void gpu_done(void) { // the submission being recorded is issued and finishes + issued++; + int k = 0; + for (int i = 0; i < nqueue; i++) + if (queue[i].fence <= issued) queue[i].fn(queue[i].arg); + else queue[k++] = queue[i]; + nqueue = k; +} + +// ---- the table as a shader reads it (the layout at the top of vk_gpa.c) +enum { L1_WORD = 16, L1_N = 65536, L2_WORD = L1_WORD + L1_N, L2_N = 4096, BLOCKS = 1024 }; +static uint32_t block_of(uint64_t addr) { return table[L1_WORD + (addr >> 24)]; } +static uint64_t read_through(uint64_t addr, int twin) { // the device address addr resolves to, 0 for the zero buffer + uint32_t blk = block_of(addr); + uint32_t rec = blk ? table[L2_WORD + blk * L2_N + ((addr >> 12) & (L2_N - 1))] : 0; + if (!rec) return 0; + const uint32_t *r = table + table[2] + rec * 4 + (twin ? 2 : 0); + return ((uint64_t)r[0] | (uint64_t)r[1] << 32) + addr; +} + +static VkBuffer new_buf(void) { return (VkBuffer)(next_handle += 0x1000); } +#define MiB (1ull << 20) + +// Every page of [lo, hi) reads buf (whose byte 0 is at buf_lo), or the zero buffer when buf is none. +static void expect(uint64_t lo, uint64_t hi, VkBuffer buf, uint64_t buf_lo, const char *what) { + for (uint64_t a = lo; a < hi; a += 4096) { + uint64_t got = read_through(a, 0), want = buf ? dev_of(buf) + (a - buf_lo) : 0; + if (got != want) { + CHECK(0, "%s: %#llx reads %#llx, expected %#llx", what, (unsigned long long)a, (unsigned long long)got, + (unsigned long long)want); + return; + } + } +} + +static uint32_t cover_rec; +static uint64_t cover_lo, cover_hi; +static uint32_t cover(uint64_t a) { return a >= cover_lo && a + 4096 <= cover_hi ? cover_rec : 0; } + +int main(void) { + CHECK(vk_gpa_on() && table && table[2], "the table exists"); + + // a buffer over three 16 MiB ranges, not aligned to them + uint64_t a0 = 0x600000000ull + 5 * MiB, a1 = a0 + 40 * MiB; + VkBuffer b0 = new_buf(); + uint32_t r0 = vk_gpa_map(a0, a1, b0, a0); + CHECK(r0, "a first buffer is mapped"); + expect(a0, a1, b0, a0, "first buffer"); + expect(a0 - 16 * MiB, a0, VK_NULL_HANDLE, 0, "below the first buffer"); + expect(a1, a1 + 16 * MiB, VK_NULL_HANDLE, 0, "above the first buffer"); + + // a mirror takes the pages with the imported buffer as twin, and gives them back + VkBuffer m0 = new_buf(); + r0 = vk_gpa_remap(a0, a1, r0, m0, a0); + expect(a0, a1, m0, a0, "mirrored"); + CHECK(read_through(a0 + 4096, 1) == dev_of(b0) + 4096, "the twin of a mirrored page is the imported buffer"); + r0 = vk_gpa_remap_plain(a0, a1, r0, b0, a0); + expect(a0, a1, b0, a0, "mirror evicted"); + CHECK(read_through(a0 + 4096, 1) == dev_of(b0) + 4096, "a plain page is its own twin"); + gpu_done(); + + // an unmapped range reads the zero buffer at once, and its blocks serve no other range while the GPU may still read + // the old L1 entries + uint32_t old_blk[3] = {block_of(a0), block_of(a0 + 16 * MiB), block_of(a0 + 32 * MiB)}; + CHECK(old_blk[0] && old_blk[1] && old_blk[2], "three blocks for three ranges"); + vk_gpa_unmap(a0, a1, r0, NULL); + expect(a0, a1, VK_NULL_HANDLE, 0, "unmapped"); + CHECK(!block_of(a0) && !block_of(a0 + 16 * MiB) && !block_of(a0 + 32 * MiB), "empty blocks leave L1"); + uint64_t c0 = 0x700000000ull; + VkBuffer b1 = new_buf(); + uint32_t r1 = vk_gpa_map(c0, c0 + 48 * MiB, b1, c0); + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + CHECK(block_of(c0 + (uint64_t)i * 16 * MiB) != old_blk[j], "a block still waiting for the GPU serves another range"); + for (int j = 0; j < 3; j++) + for (uint32_t k = 0; k < L2_N; k++) + if (table[L2_WORD + old_blk[j] * L2_N + k]) { CHECK(0, "a waiting block holds an entry"); break; } + // the same range mapped again before the GPU is done takes its block back + VkBuffer b2 = new_buf(); + uint32_t r2 = vk_gpa_map(a0, a1, b2, a0); + CHECK(block_of(a0) == old_blk[0] && block_of(a0 + 16 * MiB) == old_blk[1] && block_of(a0 + 32 * MiB) == old_blk[2], + "a range mapped again takes its waiting blocks back"); + expect(a0, a1, b2, a0, "mapped again"); + gpu_done(); // the callbacks of the first release find the blocks in use + expect(a0, a1, b2, a0, "mapped again, after the GPU"); + VkBuffer b3 = new_buf(); + uint32_t r3 = vk_gpa_map(0x780000000ull, 0x780000000ull + 16 * MiB, b3, 0x780000000ull); + for (int j = 0; j < 3; j++) CHECK(block_of(0x780000000ull) != old_blk[j], "a block taken back was freed by its old callback"); + vk_gpa_unmap(0x780000000ull, 0x780000000ull + 16 * MiB, r3, NULL); + vk_gpa_unmap(a0, a1, r2, NULL); + vk_gpa_unmap(c0, c0 + 48 * MiB, r1, NULL); + gpu_done(); + + // released, taken back and released within one submission, many times: one block, whatever the callbacks pending + for (int i = 0; i < 3000; i++) { + VkBuffer b = new_buf(); + uint32_t r = vk_gpa_map(a0, a0 + 8 * MiB, b, a0); + if (i % 500 == 0) expect(a0, a0 + 8 * MiB, b, a0, "freed and made again at its address"); + vk_gpa_unmap(a0, a0 + 8 * MiB, r, NULL); + if (i % 1000 == 999) gpu_done(); + } + gpu_done(); + CHECK(!out_of_blocks_lines, "a pool freed and made again at its address ran out of blocks"); + + // addresses that climb through 64 GiB, as the game's allocator hands them out: 20 MiB pools 48 MiB apart, eight + // alive at a time, far more ranges than there are blocks + enum { ALIVE = 8, POOLS = 1300 }; + struct { uint64_t lo, hi; VkBuffer b; uint32_t rec; } live[ALIVE] = {0}; + for (int i = 0; i < POOLS; i++) { + int s = i % ALIVE; + if (live[s].rec) vk_gpa_unmap(live[s].lo, live[s].hi, live[s].rec, NULL); + live[s].lo = 0x800000000ull + (uint64_t)i * 48 * MiB + 3 * MiB, live[s].hi = live[s].lo + 20 * MiB; + live[s].b = new_buf(); + live[s].rec = vk_gpa_map(live[s].lo, live[s].hi, live[s].b, live[s].lo); + CHECK(live[s].rec, "climbing pool %d has a record", i); + if (i % 3 == 0) gpu_done(); + if (i % 97 == 0 || i == POOLS - 1) + for (int k = 0; k < ALIVE; k++) + if (live[k].rec) expect(live[k].lo, live[k].hi, live[k].b, live[k].lo, "climbing pool"); + } + CHECK(!out_of_blocks_lines, "climbing pool addresses ran out of blocks (%d lines)", out_of_blocks_lines); + for (int k = 0; k < ALIVE; k++) vk_gpa_unmap(live[k].lo, live[k].hi, live[k].rec, NULL); + gpu_done(); + + // overlapping imports: the pages of the one that goes return to the other, and their block stays + uint64_t o0 = 0x500000000ull, o1 = o0 + 16 * MiB; + VkBuffer ba = new_buf(), bb = new_buf(); + uint32_t ra = vk_gpa_map(o0, o1, ba, o0), rb = vk_gpa_map(o0 + 4 * MiB, o0 + 8 * MiB, bb, o0 + 4 * MiB); + expect(o0 + 4 * MiB, o0 + 8 * MiB, bb, o0 + 4 * MiB, "the later import holds the shared pages"); + cover_rec = ra, cover_lo = o0, cover_hi = o1; + vk_gpa_unmap(o0 + 4 * MiB, o0 + 8 * MiB, rb, cover); + expect(o0, o1, ba, o0, "the shared pages return to the other import"); + cover_rec = 0; + vk_gpa_clear(o0 + 2 * MiB, o0 + 3 * MiB); + expect(o0 + 2 * MiB, o0 + 3 * MiB, VK_NULL_HANDLE, 0, "cleared pages"); + expect(o0, o0 + 2 * MiB, ba, o0, "pages below the cleared ones"); + CHECK(block_of(o0), "a block that still maps pages stays"); + vk_gpa_clear(o0, o1); + CHECK(!block_of(o0), "a block cleared whole leaves L1"); + vk_gpa_unmap(o0, o1, ra, NULL); + gpu_done(); + + // every block in use at once: the next range is left out and said so once, the ranges after it and the mapped + // ones are untouched, and a block given back serves the range that was left out + static struct { VkBuffer b; uint32_t rec; } all[BLOCKS]; + uint64_t f0 = 0x1000000000ull; + for (uint32_t i = 0; i < BLOCKS - 1; i++) { + all[i].b = new_buf(); + all[i].rec = vk_gpa_map(f0 + (uint64_t)i * 32 * MiB, f0 + (uint64_t)i * 32 * MiB + MiB, all[i].b, f0 + (uint64_t)i * 32 * MiB); + } + CHECK(!out_of_blocks_lines, "the table holds %u ranges at once", BLOCKS - 1); + uint64_t x0 = 0x2000000000ull; + VkBuffer bx = new_buf(); + uint32_t rx = vk_gpa_map(x0, x0 + MiB, bx, x0); + CHECK(out_of_blocks_lines == 1, "a range past the last block is reported (%d lines)", out_of_blocks_lines); + expect(x0, x0 + MiB, VK_NULL_HANDLE, 0, "a range past the last block"); + for (uint32_t i = 0; i < BLOCKS - 1; i += 37) + expect(f0 + (uint64_t)i * 32 * MiB, f0 + (uint64_t)i * 32 * MiB + MiB, all[i].b, f0 + (uint64_t)i * 32 * MiB, "a full table"); + vk_gpa_unmap(x0, x0 + MiB, rx, NULL); + vk_gpa_unmap(f0, f0 + MiB, all[0].rec, NULL); + rx = vk_gpa_map(x0, x0 + MiB, bx, x0); + expect(x0, x0 + MiB, VK_NULL_HANDLE, 0, "a block the GPU may still read"); + vk_gpa_unmap(x0, x0 + MiB, rx, NULL); + gpu_done(); + rx = vk_gpa_map(x0, x0 + MiB, bx, x0); + expect(x0, x0 + MiB, bx, x0, "a block given back"); + + vk_gpa_report(); + printf("gpa_check: %s\n", fails ? "FAILED" : "ok"); + return fails != 0; +} diff --git a/tests/gpa_check.py b/tests/gpa_check.py new file mode 100644 index 00000000..7a5d0ba0 --- /dev/null +++ b/tests/gpa_check.py @@ -0,0 +1,47 @@ +"""Build and run tests/gpa_check.c: runtime/vk_gpa.c's device address table, with blocks given back and taken again. + +Needs the Vulkan headers and vk_mem_alloc.h (docs/building.md), no GPU and no game files. + +Usage: gpa_check.py +""" +import os +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MSYS_BIN = Path(r'C:\msys64\ucrt64\bin') + + +def compiler(): + """$CC, else MSYS2 UCRT64's clang, else clang on PATH (as tests/diff/common.py picks it, without its Unicorn).""" + if os.environ.get('CC'): + return os.environ['CC'] + if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists(): + return str(MSYS_BIN / 'clang.exe') + return shutil.which('clang') or 'clang' + + +def vma_include(): + for d in (MSYS_BIN.parent / 'include' / 'vma', ROOT / 'toolchain' / 'vma', Path('/usr/include')): + if (d / 'vk_mem_alloc.h').is_file(): + return d + sys.exit('gpa_check: vk_mem_alloc.h not found (docs/building.md)') + + +def main(): + work = ROOT / 'build' / 'test' / 'gpa' + work.mkdir(parents=True, exist_ok=True) + exe = work / ('gpa_check.exe' if os.name == 'nt' else 'gpa_check') + env = dict(os.environ) + if os.name == 'nt' and MSYS_BIN.exists(): + env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') + subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-Wall', '-fno-strict-aliasing', '-I', str(ROOT / 'runtime'), + '-I', str(vma_include()), str(ROOT / 'tests' / 'gpa_check.c'), + str(ROOT / 'runtime' / 'vk_gpa.c'), '-o', str(exe)], env=env) + return subprocess.run([str(exe)], env=env).returncode + + +if __name__ == '__main__': + sys.exit(main())