From c2225a3f6161d457d6759b5bef545b6e3686b48b Mon Sep 17 00:00:00 2001 From: SkippySteve Date: Sat, 7 Nov 2026 19:20:22 -0500 Subject: [PATCH] Skip repeated buffer, uniform, and pass-hazard lookups. A read-only bind is remembered until its pool changes. Uniform words that sit outside an inline update keep the gap around them, and the word memo stays. A pass keeps a mask of the targets earlier draws sampled. --- .github/workflows/source-regressions.yml | 15 ++++ gpu/vk/vk_draw.cpp | 24 ++++-- gpu/vk/vk_memory.cpp | 42 ++++++++-- gpu/vk/vk_replay.cpp | 66 ++++++++++++++-- tests/test_bind_memo.py | 98 ++++++++++++++++++++++++ tests/test_pass_hazard.py | 69 +++++++++++++++++ tests/test_ub_gap.py | 72 +++++++++++++++++ 7 files changed, 368 insertions(+), 18 deletions(-) create mode 100644 tests/test_bind_memo.py create mode 100644 tests/test_pass_hazard.py create mode 100644 tests/test_ub_gap.py diff --git a/.github/workflows/source-regressions.yml b/.github/workflows/source-regressions.yml index a61e450d..6baa9a26 100644 --- a/.github/workflows/source-regressions.yml +++ b/.github/workflows/source-regressions.yml @@ -22,6 +22,7 @@ on: - tests/test_fps_rate.py - gpu/vk/vk_memory.cpp - tests/test_vertex_bind.py + - tests/test_bind_memo.py - tests/test_fps_overlay_order.py - tests/test_dispatch_clock.py - gpu/vk/vk_framegen.cpp @@ -37,9 +38,11 @@ on: - gpu/vk/vk_replay.cpp - gpu/vk/vk_draw.h - tests/test_ub_memo.py + - tests/test_ub_gap.py - tests/test_shader_census_cmd.py - gpu/vk/vk_shader.cpp - tests/test_same_draw.py + - tests/test_pass_hazard.py - nx2/version.py - nx2/test_version.py - host/nn_audio.c @@ -80,6 +83,7 @@ on: - tests/test_fps_rate.py - gpu/vk/vk_memory.cpp - tests/test_vertex_bind.py + - tests/test_bind_memo.py - tests/test_fps_overlay_order.py - tests/test_dispatch_clock.py - gpu/vk/vk_framegen.cpp @@ -95,9 +99,11 @@ on: - gpu/vk/vk_replay.cpp - gpu/vk/vk_draw.h - tests/test_ub_memo.py + - tests/test_ub_gap.py - tests/test_shader_census_cmd.py - gpu/vk/vk_shader.cpp - tests/test_same_draw.py + - tests/test_pass_hazard.py - nx2/version.py - nx2/test_version.py - host/nn_audio.c @@ -164,6 +170,9 @@ jobs: - name: Check a vertex stream is bound once if: runner.os == 'Linux' run: python -I -B tests/test_vertex_bind.py + - name: Check a read-only buffer bind is remembered + if: runner.os == 'Linux' + run: python -I -B tests/test_bind_memo.py - name: Check FPS overlay transfer ordering if: runner.os == 'Linux' run: python -I -B tests/test_fps_overlay_order.py @@ -189,6 +198,12 @@ jobs: - name: Check a repeated uniform word is not read again if: runner.os == 'Linux' run: python -I -B tests/test_ub_memo.py + - name: Check a uniform word outside the updates is not scanned again + if: runner.os == 'Linux' + run: python -I -B tests/test_ub_gap.py + - name: Check a pass hazard skips textures that are not targets + if: runner.os == 'Linux' + run: python -I -B tests/test_pass_hazard.py - name: Check the shader list's text encoding run: python -I -B tests/test_shader_census_cmd.py - name: Check the build's version stamp diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index 0aeaed65..7a867542 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -2114,6 +2114,7 @@ struct Pass { uint64_t first_prog = 0; VkFormat format0 = VK_FORMAT_UNDEFINED; std::vector sampled; // (rendered textures its draws sampled: a target among them is a hazard) + uint16_t sampled_targets = 0; // (colour i bit i, depth bit 8: targets earlier draws sampled) uint16_t written = 0; // (its targets written so far: colour i bit i, depth bit 8) }; thread_local Pass pass; @@ -2247,7 +2248,7 @@ void begin_pass(VkCommandBuffer cmd, const Targets& at) { pass.draws = 0, pass.format0 = at.n ? at.color_format[0] : at.depth_format; pass.wm_or = 0, pass.wm_n = 0; pass.clears = 0, pass.first_prog = 0; - pass.sampled.clear(), pass.written = 0; + pass.sampled.clear(), pass.sampled_targets = 0, pass.written = 0; vkCmdBeginRendering(cmd, &ri); pass.open = true; st_passes++; @@ -2617,6 +2618,9 @@ void clear_after_draws(VkCommandBuffer cmd) { } int64_t st_pass_hazards; +bool writes_sampled_target(uint16_t writes, uint16_t sampled_targets) { + return (writes & sampled_targets) != 0; +} /// A draw that samples a target this pass already wrote, or writes a target an earlier draw of this pass sampled: in /// a pass of its own, after a full barrier (begin_pass makes it), as for the clears above: in one pass the GPU orders /// the targets' writes with each other only, not with the draws' texture reads of them. (A draw that samples a target @@ -2641,11 +2645,8 @@ uint16_t draw_hazards(VkCommandBuffer cmd, const Program& p, const NvnState& s, const int k = slot_of(t); if (k >= 0 && (pass.written >> k & 1) && !(writes >> k & 1)) { kind = "samples a target the pass wrote"; break; } } - if (!kind) - for (const Texture* t : pass.sampled) { - const int k = slot_of(t); - if (k >= 0 && (writes >> k & 1)) { kind = "writes a target an earlier draw sampled"; break; } - } + if (!kind && writes_sampled_target(writes, pass.sampled_targets)) + kind = "writes a target an earlier draw sampled"; if (!kind) return writes; static int logged; if (logged++ < 8) @@ -4104,8 +4105,15 @@ void draw(VkCommandBuffer cmd, NvnState& s, const DrawArgs& d) { probe_draw(s, p, pass.draws); // (hookshot: RX_VK_PROBE) pass.prog = p.key; if (!pass.first_prog) pass.first_prog = p.key; - for (const Texture* t : gfx_sampled) - if (std::find(pass.sampled.begin(), pass.sampled.end(), t) == pass.sampled.end()) pass.sampled.push_back(t); + for (const Texture* t : gfx_sampled) { + if (std::find(pass.sampled.begin(), pass.sampled.end(), t) != pass.sampled.end()) continue; + pass.sampled.push_back(t); + int slot = -1; + for (int i = 0; i < at.n; i++) + if (at.color[i] == t) { slot = i; break; } + if (slot < 0 && at.depth == t) slot = 8; + if (slot >= 0) pass.sampled_targets |= (uint16_t)(1u << slot); + } pass.written |= writes; if (pl != last_pipeline) vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pl), last_pipeline = pl; if (!same_desc) { diff --git a/gpu/vk/vk_memory.cpp b/gpu/vk/vk_memory.cpp index edd83f42..5e9a21a9 100644 --- a/gpu/vk/vk_memory.cpp +++ b/gpu/vk/vk_memory.cpp @@ -1712,6 +1712,26 @@ uint64_t gpu_bind_stamp() { return pools_gen.load(std::memory_order_relaxed) ^ (mirror_epoch * 0x9E3779B97F4A7C15ull); } +uint32_t bind_slot(uint64_t guest, uint64_t size) { + // (a page-aligned range's low bits are zero: fold the rest in, or every one of them lands in one slot) + uint64_t h = guest ^ (size * 0x9E3779B97F4A7C15ull); + h *= 0x9E3779B97F4A7C15ull; + h ^= h >> 32; + h ^= h >> 16; + return (uint32_t)(h & 255u); +} + +bool bind_memo_hit(uint64_t guest, uint64_t size, uint64_t stamp, uint64_t cached_guest, uint64_t cached_size, + uint64_t cached_stamp, int has_buffer) { + return has_buffer && size && guest == cached_guest && size == cached_size && stamp == cached_stamp; +} + +struct BindMemo { + uint64_t guest = 0, size = 0, stamp = 0; + BufferRef ref{}; +}; +thread_local BindMemo bind_memo[256]; + BufferRef gpu_buffer_bind(uint64_t gpu, uint64_t size, bool written) { if (written) texture_gpu_wrote(gpu, size); // (below 1 TB a GPU address is the guest address itself, and a pool holds the whole range or nothing: the host's @@ -1722,6 +1742,18 @@ BufferRef gpu_buffer_bind(uint64_t gpu, uint64_t size, bool written) { if (!host->gpu_ptr(gpu, &avail) || avail < size) return {}; guest = host->gpu_guest(gpu); } + // (the same read, while that stamp still stands: the buffer and the offset are the ones found before. a miss + // runs the lookup below, including a mirror sync, and remembers what it returned. a write is not remembered) + auto remember = [&](BufferRef ref) { + if (!written && ref.buffer != VK_NULL_HANDLE && size) + bind_memo[bind_slot(guest, size)] = BindMemo{guest, size, gpu_bind_stamp(), ref}; + return ref; + }; + if (!written) { + const BindMemo& m = bind_memo[bind_slot(guest, size)]; + if (bind_memo_hit(guest, size, gpu_bind_stamp(), m.guest, m.size, m.stamp, m.ref.buffer != VK_NULL_HANDLE)) + return m.ref; + } // (a read binding of a pool this thread found before and whose mirror is in use this submission or that has none, // without the lock: every thread's, the replay thread's too (each draw binds several uniform buffers: the pool's // lookup under the lock was ~8% of the replay's time in a race); the rest under it, as below) @@ -1733,9 +1765,9 @@ BufferRef gpu_buffer_bind(uint64_t gpu, uint64_t size, bool written) { const uint64_t off = guest - p->lo; if (p->last_value < mem_rec_value) p->last_value = mem_rec_value; if (!mirror_on() || !mirror_upload || p->mstate == Pool::MIRROR_REFUSED || p->mstate == Pool::MIRROR_WANTED) - return {p->buffer, off, size, 0}; + return remember({p->buffer, off, size, 0}); if (p->mstate == Pool::MIRROR_ON && p->synced_epoch == mirror_epoch && p->sync_seg <= texture_segment()) - return {p->mirror, off, size, 0}; + return remember({p->mirror, off, size, 0}); } ParLock plk; // (before pools_mu, always: a mirror sync records into the shared upload command buffer) std::lock_guard lk(pools_mu); @@ -1747,7 +1779,7 @@ BufferRef gpu_buffer_bind(uint64_t gpu, uint64_t size, bool written) { const uint64_t off = guest - p->lo; if (!mirror_on() || !mirror_upload || p->mstate == Pool::MIRROR_REFUSED) { if (mirror_on()) { auto& e = direct_by_pool[p->lo]; e.first++, e.second = mirror_upload ? (int)p->mstate : 9; } - return {p->buffer, off, size, 0}; + return remember({p->buffer, off, size, 0}); } // (a pool's mirror is made at its first binding, in this part's uploads: bound in guest memory until the next // submission, the GPU read it there while the game's threads wrote it for their next frames, and a pass that drew @@ -1772,11 +1804,11 @@ BufferRef gpu_buffer_bind(uint64_t gpu, uint64_t size, bool written) { if (written) p->direct_write_epoch = mirror_epoch; if (written && size <= (1u << 20)) // (EvilBoyce: GPU results written in place; small ones: the readbacks are) wb_pages_note(p->lo + off, p->lo + std::min(off + size, p->hi - p->lo), mem_rec_value); - return {p->buffer, off, size, 0}; + return remember({p->buffer, off, size, 0}); } if (p->synced_epoch != mirror_epoch || p->sync_seg > texture_segment()) mirror_sync(*p); if (written) writebacks.push_back({p, off, std::min(off + size, p->hi - p->lo)}); - return {p->mirror, off, size, 0}; + return remember({p->mirror, off, size, 0}); } // ---- mirrors given back ---- diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index bf9cdece..8984b55f 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -306,7 +306,10 @@ void ub_prune(uint64_t done) { if (ub_first_of_recording) ub_first_of_recording--; dropped = true; } - if (dropped) ub_prune_gen.fetch_add(1, std::memory_order_relaxed); + if (dropped) { + ub_prune_gen.fetch_add(1, std::memory_order_relaxed); + ub_list_epoch.fetch_add(1, std::memory_order_relaxed); // (a gap was measured against the old list) + } } } // namespace @@ -314,6 +317,49 @@ void ub_prune(uint64_t done) { uint64_t ub_generation() { return ub_gen; } +/// The open gap around [addr, addr+bytes) that no update covers. False when one does. [lo, hi) is that gap. +bool ub_gap(const uint64_t* dst, const uint64_t* end, int n, uint64_t addr, uint64_t bytes, uint64_t* lo, uint64_t* hi) { + if (!bytes || addr + bytes < addr) return false; + uint64_t left = 0, right = ~uint64_t{0}; + const uint64_t stop = addr + bytes; + for (int i = 0; i < n; i++) { + if (dst[i] < stop && addr < end[i]) return false; + if (end[i] <= addr && end[i] > left) left = end[i]; + if (dst[i] >= stop && dst[i] < right) right = dst[i]; + } + *lo = left; + *hi = right; + return true; +} +struct UbGapCache { + uint64_t epoch = 0, lo = 0, hi = 0; + size_t first = 0, lim = 0; + bool on = false; +}; +thread_local UbGapCache ub_gap_cache; +bool ub_gap_holds(uint64_t addr, uint64_t n, size_t first, size_t lim) { + return n && ub_gap_cache.on && ub_gap_cache.epoch == ub_list_epoch.load(std::memory_order_relaxed) && + ub_gap_cache.first == first && ub_gap_cache.lim == lim && addr >= ub_gap_cache.lo && + addr + n >= addr && addr + n <= ub_gap_cache.hi; +} +bool ub_try_gap(size_t first, size_t lim, uint64_t addr, uint64_t n) { + if (first > lim) return false; + const uint64_t epoch = ub_list_epoch.load(std::memory_order_relaxed); + static thread_local std::vector dst, end; + dst.clear(); + end.clear(); + dst.reserve(lim - first); + end.reserve(lim - first); + for (size_t i = first; i < lim; i++) { + dst.push_back(ub_updates[i].dst); + end.push_back(ub_updates[i].dst + ub_updates[i].bytes.size()); + } + uint64_t lo = 0, hi = 0; + if (!ub_gap(dst.data(), end.data(), (int)dst.size(), addr, n, &lo, &hi)) return false; + if (ub_list_epoch.load(std::memory_order_relaxed) != epoch) return false; // (the list changed: do not keep the gap) + ub_gap_cache = {epoch, lo, hi, first, lim, true}; + return true; +} /// No update in [have, lim) overlaps [addr, addr + n). have == lim: nothing new. bool ub_tail_clear(size_t have, size_t lim, uint64_t addr, uint64_t n, const uint64_t* dst, const uint64_t* len, size_t count) { @@ -389,6 +435,11 @@ uint64_t ub_overlay(uint64_t addr, uint64_t n, uint8_t* base, const std::functio const uint64_t view = memory_capture_view(); uint64_t kept = 0; if (ub_copy_hit(addr, n, base, first, lim, &kept)) return kept; + // (any range in the gap the last miss measured: no update covers it, so the captured bytes stand) + if (ub_gap_holds(addr, n, first, lim) || ub_try_gap(first, lim, addr, n)) { + ub_copy_keep(addr, n, base, first, lim, 0, true, view); + return UINT64_MAX; + } bool any = false; for (size_t i = first; i < lim; i++) { const UbUpdate& u = ub_updates[i]; @@ -430,10 +481,15 @@ bool ub_read(uint64_t addr, void* dst, uint64_t n) { const uint32_t page = n == 4 ? memory_page_stamp(addr) : 0; const uint64_t prune = ub_prune_gen.load(std::memory_order_relaxed); if (!memory_read_as_submitted(addr, dst, n)) return false; - for (size_t i = 0; i < lim; i++) { - const UbUpdate& u = ub_updates[i]; - const uint64_t lo = std::max(addr, u.dst), hi = std::min(addr + n, u.dst + u.bytes.size()); - if (lo < hi) std::memcpy((uint8_t*)dst + (lo - addr), u.bytes.data() + (lo - u.dst), hi - lo); + // (most words are not inside an inline update: the scan that finds that is kept for the gap, and the next words + // in the gap do not walk the list. the captured bytes are still the answer) + const bool in_gap = ub_gap_holds(addr, n, 0, lim) || ub_try_gap(0, lim, addr, n); + if (!in_gap) { + for (size_t i = 0; i < lim; i++) { + const UbUpdate& u = ub_updates[i]; + const uint64_t lo = std::max(addr, u.dst), hi = std::min(addr + n, u.dst + u.bytes.size()); + if (lo < hi) std::memcpy((uint8_t*)dst + (lo - addr), u.bytes.data() + (lo - u.dst), hi - lo); + } } if (n == 4) { UbWord& w = ub_words[(addr >> 2) & 127]; diff --git a/tests/test_bind_memo.py b/tests/test_bind_memo.py new file mode 100644 index 00000000..75e1f562 --- /dev/null +++ b/tests/test_bind_memo.py @@ -0,0 +1,98 @@ +"""A repeated read-only buffer bind skips the pool lookup. + +Draws bind the same guest ranges again: vertex streams, an index buffer, indirect +arguments, uniform buffers, storage-buffer entries. The binding stands until a pool +leaves, a mirror is made, or the next submission starts. +python -I -B tests/test_bind_memo.py [--cxx g++] +""" +import argparse +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def block(text, marker): + start = text.index(marker) + end = text.index("{", start) + 1 + depth = 1 + while depth: + depth += (text[end] == "{") - (text[end] == "}") + end += 1 + return text[start:end] + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--cxx", default="g++") + a = p.parse_args() + source = (ROOT / "gpu" / "vk" / "vk_memory.cpp").read_text(encoding="utf-8") + bind = block(source, "BufferRef gpu_buffer_bind(") + lock = bind.index("ParLock") + if bind.index("bind_memo_hit(") > lock: + raise SystemExit("a read bind takes the pool lock before checking the remembered one") + if "if (!written)" not in bind[: bind.index("bind_memo_hit(")]: + raise SystemExit("a written bind can be served from the remembered read") + store = bind.index("bind_memo[bind_slot(guest, size)] =") + if "gpu_bind_stamp()" not in bind[store : store + 160]: + raise SystemExit("a remembered bind keeps the stamp from before the lookup") + if "if (!p) return {};" not in bind: + raise SystemExit("a miss is remembered") + stamp = block(source, "uint64_t gpu_bind_stamp(") + for needle in ("pools_gen", "mirror_epoch"): + if needle not in stamp: + raise SystemExit(f"a remembered bind ignores {needle}") + made = block(source, "bool mirror_make(Pool& pool) {") + on = made.index("p->mstate = Pool::MIRROR_ON;") + nxt = made.index("p->synced_gen", on) + if "pools_gen" not in made[on:nxt]: + raise SystemExit("a new mirror does not drop remembered bindings") + slot = block(source, "uint32_t bind_slot(") + hit = block(source, "bool bind_memo_hit(") + fixture = f'''#include +#include +#include +#define CHECK(x) do {{ if (!(x)) {{ std::fprintf(stderr, "FAIL:%d %s\\n", __LINE__, #x); std::exit(1); }} }} while (0) +{slot} +{hit} +int main() {{ + CHECK(bind_slot(0x1000, 64) == bind_slot(0x1000, 64)); + CHECK(bind_slot(0x1000, 64) != bind_slot(0x1000, 128)); + CHECK(bind_slot(0, 64) != bind_slot(1ull << 40, 64)); + int seen[256] = {{}}; + int n = 0; + for (uint64_t i = 0; i < 64; i++) {{ + const uint32_t s = bind_slot(i << 12, 64); + CHECK(s < 256); + if (!seen[s]) n++, seen[s] = 1; + }} + CHECK(n > 16); + int near[256] = {{}}; + int n_near = 0; + for (uint64_t i = 0; i < 32; i++) {{ + const uint32_t s = bind_slot(0x100000000ull + i * 16, 64); + CHECK(s < 256); + if (!near[s]) n_near++, near[s] = 1; + }} + CHECK(n_near > 8); + CHECK(bind_memo_hit(0x1000, 64, 7, 0x1000, 64, 7, 1)); + CHECK(bind_memo_hit(0, 16, 1, 0, 16, 1, 1)); + CHECK(!bind_memo_hit(0x1000, 64, 7, 0x1000, 64, 8, 1)); + CHECK(!bind_memo_hit(0x1000, 32, 7, 0x1000, 64, 7, 1)); + CHECK(!bind_memo_hit(0x2000, 64, 7, 0x1000, 64, 7, 1)); + CHECK(!bind_memo_hit(0x1000, 64, 7, 0x1000, 64, 7, 0)); + CHECK(!bind_memo_hit(0, 0, 1, 0, 0, 1, 1)); + std::printf("PASS: a read bind is reused only for that range and stamp\\n"); +}} +''' + with tempfile.TemporaryDirectory(prefix="bind-memo-") as tmp: + cpp = Path(tmp) / "fixture.cpp" + exe = Path(tmp) / "fixture" + cpp.write_text(fixture, encoding="utf-8") + subprocess.run([a.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(cpp), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tests/test_pass_hazard.py b/tests/test_pass_hazard.py new file mode 100644 index 00000000..2d223489 --- /dev/null +++ b/tests/test_pass_hazard.py @@ -0,0 +1,69 @@ +"""A pass hazard does not walk every texture the pass has sampled. + +Earlier draws record the targets they sampled as bits. A later draw that writes one +of those targets is the hazard. python -I -B tests/test_pass_hazard.py [--cxx g++] +""" +import argparse +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def block(text, marker): + start = text.index(marker) + end = text.index("{", start) + 1 + depth = 1 + while depth: + depth += (text[end] == "{") - (text[end] == "}") + end += 1 + return text[start:end] + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--cxx", default="g++") + a = p.parse_args() + source = (ROOT / "gpu" / "vk" / "vk_draw.cpp").read_text(encoding="utf-8") + haz = block(source, "uint16_t draw_hazards(") + if "for (const Texture* t : pass.sampled)" in haz: + raise SystemExit("a pass hazard walks every texture sampled so far") + if "writes_sampled_target(" not in haz: + raise SystemExit("a pass hazard ignores the targets earlier draws sampled") + if haz.index("gfx_sampled") > haz.index("writes_sampled_target("): + raise SystemExit("a draw's own samples are treated as an earlier draw's") + begin = block(source, "void begin_pass(") + clear = begin.index("pass.sampled.clear()") + if "sampled_targets = 0" not in begin[clear:clear + 80]: + raise SystemExit("a new pass keeps the targets earlier draws sampled") + ins = source.index("pass.sampled.push_back(t);") + wrote = source.index("pass.written |= writes;", ins) + if "sampled_targets" not in source[ins:wrote]: + raise SystemExit("a sampled target is invisible to the next draw") + fn = block(source, "bool writes_sampled_target(") + fixture = f'''#include +#include +#include +#define CHECK(x) do {{ if (!(x)) {{ std::fprintf(stderr, "FAIL:%d %s\\n", __LINE__, #x); std::exit(1); }} }} while (0) +{fn} +int main() {{ + CHECK(writes_sampled_target(1u, 1u)); + CHECK(writes_sampled_target(0x100u, 0x100u)); + CHECK(writes_sampled_target(0x5u, 0x4u)); + CHECK(!writes_sampled_target(1u, 2u)); + CHECK(!writes_sampled_target(0u, 0x1FFu)); + CHECK(!writes_sampled_target(0x1FFu, 0u)); + std::printf("PASS: a draw hazards only the targets earlier draws sampled\\n"); +}} +''' + with tempfile.TemporaryDirectory(prefix="pass-hazard-") as tmp: + cpp = Path(tmp) / "fixture.cpp" + exe = Path(tmp) / "fixture" + cpp.write_text(fixture, encoding="utf-8") + subprocess.run([a.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(cpp), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tests/test_ub_gap.py b/tests/test_ub_gap.py new file mode 100644 index 00000000..05f363c1 --- /dev/null +++ b/tests/test_ub_gap.py @@ -0,0 +1,72 @@ +"""Words outside an inline uniform update do not walk the update list again. + +Most descriptor words are not inside an UPDATE_UB. The first miss remembers the gap +around it. python -I -B tests/test_ub_gap.py [--cxx g++] +""" +import argparse +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def block(text, marker): + start = text.index(marker) + end = text.index("{", start) + 1 + depth = 1 + while depth: + depth += (text[end] == "{") - (text[end] == "}") + end += 1 + return text[start:end] + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--cxx", default="g++") + a = p.parse_args() + source = (ROOT / "gpu" / "vk" / "vk_replay.cpp").read_text(encoding="utf-8") + fn = block(source, "bool ub_gap(") + read = block(source, "bool ub_read(") + holds = read.index("ub_gap_holds(") + scan = read.index("for (size_t i = 0; i < lim; i++)") + if holds > scan: + raise SystemExit("ub_read walks the updates before checking the gap") + if "memory_read_as_submitted(" not in read[:holds]: + raise SystemExit("a word in the gap is answered without the captured bytes") + if "ub_list_epoch.fetch_add" not in block(source, "void ub_prune("): + raise SystemExit("dropping an update leaves the gap in place") + push = source[source.index("ub_updates.push_back(std::move(up));"):source.index("ub_updates.push_back(std::move(up));") + 180] + if "ub_list_epoch.fetch_add" not in push: + raise SystemExit("adding an update leaves the gap in place") + fixture = f'''#include +#include +#include +#define CHECK(x) do {{ if (!(x)) {{ std::fprintf(stderr, "FAIL:%d %s\\n", __LINE__, #x); std::exit(1); }} }} while (0) +{fn} +int main() {{ + const uint64_t dst[] = {{100, 200}}; + const uint64_t end[] = {{110, 210}}; + uint64_t lo = 1, hi = 1; + CHECK(ub_gap(dst, end, 2, 150, 4, &lo, &hi)); + CHECK(lo == 110 && hi == 200); + CHECK(ub_gap(dst, end, 2, 110, 90, &lo, &hi)); + CHECK(lo == 110 && hi == 200); + CHECK(!ub_gap(dst, end, 2, 108, 4, &lo, &hi)); + CHECK(!ub_gap(dst, end, 2, 190, 20, &lo, &hi)); + CHECK(ub_gap(dst, end, 0, 5, 5, &lo, &hi)); + CHECK(lo == 0 && hi == ~uint64_t{{0}}); + CHECK(!ub_gap(dst, end, 2, ~uint64_t{{0}} - 2, 8, &lo, &hi)); + std::printf("PASS: a miss remembers the gap until the next update\\n"); +}} +''' + with tempfile.TemporaryDirectory(prefix="ub-gap-") as tmp: + cpp = Path(tmp) / "fixture.cpp" + exe = Path(tmp) / "fixture" + cpp.write_text(fixture, encoding="utf-8") + subprocess.run([a.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(cpp), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + + +if __name__ == "__main__": + main()