From b7684ba8fa74be997ae076e66f143469fef78f52 Mon Sep 17 00:00:00 2001 From: SkippySteve Date: Sat, 7 Nov 2026 20:28:05 -0500 Subject: [PATCH] Reuse a program's descriptors after another program was drawn. A draw kept descriptors only when it repeated the previous draw's program. Binding another program dropped them, so almost every draw built its set again. The set is kept per program and pushed again when that program returns, unless a binding, a registration, a mirror, or a uniform update it read has changed. --- .github/workflows/source-regressions.yml | 5 + gpu/vk/vk_draw.cpp | 288 +++++++++++++++++++++-- gpu/vk/vk_draw.h | 13 + gpu/vk/vk_replay.cpp | 45 +++- gpu/vk/vk_state.h | 1 + tests/test_desc_prog.py | 111 +++++++++ 6 files changed, 437 insertions(+), 26 deletions(-) create mode 100644 tests/test_desc_prog.py diff --git a/.github/workflows/source-regressions.yml b/.github/workflows/source-regressions.yml index d943751fd..68c460fa9 100644 --- a/.github/workflows/source-regressions.yml +++ b/.github/workflows/source-regressions.yml @@ -24,6 +24,7 @@ on: - tests/test_fps_rate.py - gpu/vk/vk_memory.cpp - tests/test_vertex_bind.py + - tests/test_desc_prog.py - tests/test_fps_overlay_order.py - tests/test_dispatch_clock.py - gpu/vk/vk_framegen.cpp @@ -95,6 +96,7 @@ on: - tests/test_fps_rate.py - gpu/vk/vk_memory.cpp - tests/test_vertex_bind.py + - tests/test_desc_prog.py - tests/test_fps_overlay_order.py - tests/test_dispatch_clock.py - gpu/vk/vk_framegen.cpp @@ -192,6 +194,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 program keeps its descriptors + if: runner.os == 'Linux' + run: python -I -B tests/test_desc_prog.py - name: Check FPS overlay transfer ordering if: runner.os == 'Linux' run: python -I -B tests/test_fps_overlay_order.py diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index 11f021136..a792f06b4 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -46,6 +46,51 @@ namespace mkwvk { struct PointerEntry { uint64_t addr, size; }; const PointerEntry* pointer_table(const NvnState& s, int stage); uint32_t stage_cbuf_word(const NvnState& s, const Stage& st, uint32_t index, uint32_t offset); // (vk_shader.cpp) + +// (ranges a descriptor build reads from uniform memory. Off, desc_note_range is one load. Outside the file's +// private namespace: the replay's uniform reads call it) +thread_local bool desc_watch_on = false; +struct DescWatch { + bool overflow = false; + std::vector> ranges; + std::vector colors; + std::vector images, written; + std::vector> stamps; +}; +thread_local DescWatch desc_watch; +void desc_note_range_slow(uint64_t addr, uint64_t n) { + if (!n) return; + if (addr + n < addr) { + desc_watch.overflow = true; + return; + } + if (!desc_watch.ranges.empty()) { + auto& b = desc_watch.ranges.back(); + const uint64_t hi = b.first + b.second; + if (hi >= b.first && addr <= hi && b.first <= addr + n) { + const uint64_t lo = std::min(b.first, addr); + b.second = std::max(hi, addr + n) - lo; + b.first = lo; + return; + } + } + if (desc_watch.ranges.size() >= 48) { + desc_watch.overflow = true; + return; + } + desc_watch.ranges.emplace_back(addr, n); +} +void desc_watch_begin() { + desc_watch_on = true; + desc_watch = {}; +} +void desc_watch_color(const Texture* t) { if (desc_watch_on && t) desc_watch.colors.push_back(t); } +void desc_watch_image(Texture* t) { if (desc_watch_on && t) desc_watch.images.push_back(t); } +void desc_watch_written(Texture* t) { if (desc_watch_on && t) desc_watch.written.push_back(t); } +void desc_watch_stamp(uint64_t guest, uint64_t n) { + if (desc_watch_on && guest && n) desc_watch.stamps.emplace_back(guest, n); +} + namespace { bool no_a2c() { // (RX_VK_NO_A2C, read once: it was a getenv on every alpha-to-coverage draw, the foliage's) @@ -568,6 +613,7 @@ struct PipeEntry { }; std::unordered_map> pipelines; // (the replay thread's) int64_t st_draws, st_skipped, st_dispatches, st_passes, st_mesh, st_pending_skips, st_waits; + // (hookshot) skips by the line that made them, reported with the frame statistics (draw_skip_report) namespace { struct SkipSite { std::atomic line{0}; std::atomic n{0}; int64_t last = 0; }; @@ -1784,7 +1830,11 @@ void build_descriptors(const NvnState& s, const Program& p) { if (b && d.is_written) { const auto t1 = slow_desc_timing() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; const uint64_t guest = host->gpu_guest(base); - if (guest) { ParLock lk; memory_stamp_gpu(guest, n); } + if (guest) { + ParLock lk; + memory_stamp_gpu(guest, n); + desc_watch_stamp(guest, n); + } if (slow_desc_timing()) slow_descriptor(t1, "stamp", base, n); } } @@ -1846,6 +1896,7 @@ void build_descriptors(const NvnState& s, const Program& p) { (unsigned long long)p.key, k, handle, tex ? tex->id : 0, (unsigned long long)(tex ? tex->gpu : 0), tex ? tex->width : 0, tex ? tex->height : 0, tex ? tex->layers : 0, tex ? (void*)tex->image : nullptr, (void*)view, view ? "" : " (dummy)"); + if (view && tex && desc_watch_on) desc_watch_color(tex); if (view && tex && tex->gpu_written) draw_sampled.push_back(tex); if (!view) view = dummies.view[(int)vt], st_dummy_textures += handle != 0; dw.imgs.push_back({sampler_for((int)(handle >> 20), d.is_depth), view, VK_IMAGE_LAYOUT_GENERAL}); @@ -1863,8 +1914,15 @@ void build_descriptors(const NvnState& s, const Program& p) { uint32_t level = 0; VkImageView view = handle ? texture_view_of(handle, vt, ViewUse::Storage, image_format(d.format), &tex, &level) : VK_NULL_HANDLE; - if (view && tex && d.is_written) { ParLock lk; texture_written(tex); } - if (view && tex) draw_sampled.push_back(tex); + if (view && tex && d.is_written) { + ParLock lk; + texture_written(tex); + desc_watch_written(tex); + } + if (view && tex) { + draw_sampled.push_back(tex); + if (desc_watch_on) desc_watch_image(tex); + } if (log_img_wanted(p.key)) { static std::atomic told{0}; if (told++ < 16) @@ -1891,6 +1949,187 @@ void build_descriptors(const NvnState& s, const Program& p) { } } +// A program's descriptors, pushed again when that program returns after another one was drawn. The neighbour +// check still skips the push when this program was the draw before. A binding other than the program, a texture +// registration, a mirror, or a uniform range this build read, drops the set. So does a page stamped in one of +// those ranges. The push still happens: the command buffer is holding the other program's descriptors. +struct DescWrite { + VkWriteDescriptorSet head{}; + uint32_t first = 0; + uint8_t kind = 0; // 1 buffer, 2 image, 3 texel view +}; +struct DescProg { + bool on = false, any_update_misses = false, grid_matters = false; + uint64_t desc_gen = 0, reg_gen = 0, stamp = 0, destroy_gen = 0; + uint32_t mem_gen = 0, grid[3]{}; + size_t ub_mark = 0; + std::vector> ranges; + std::vector bufs; + std::vector imgs; + std::vector views; + std::vector writes; + std::vector colors; + std::vector images, written; + std::vector> stamps; + struct Ptr { int stage = -1; PointerEntry e[3]{}; }; + std::vector ptrs; // (compute, task and mesh: c0's pointer table, which a program switch does not invalidate) +}; +thread_local std::unordered_map desc_progs; +int64_t st_desc_prog; + +void desc_merge_ranges(std::vector>& r) { + if (r.size() < 2) return; + std::sort(r.begin(), r.end()); + size_t w = 0; + for (size_t i = 1; i < r.size(); i++) { + const uint64_t hi = r[w].first + r[w].second; + if (r[i].first <= hi) r[w].second = std::max(hi, r[i].first + r[i].second) - r[w].first; + else r[++w] = r[i]; + } + r.resize(w + 1); +} +template +int desc_index(const T* p, const std::vector& v) { + if (!p) return 0; + const T* b = v.data(); + if (!b || p < b || p > b + v.size()) return -1; + return (int)(p - b); +} +void desc_prog_keep(const Program& p, const NvnState& s, uint64_t reg, uint64_t stamp, uint64_t destroy_gen, size_t mark) { + DescProg d; + d.on = true; + d.desc_gen = s.desc_gen; + d.reg_gen = reg; + d.stamp = stamp; + d.destroy_gen = destroy_gen; + d.mem_gen = memory_generation(); + d.ub_mark = mark; + d.any_update_misses = desc_watch.overflow; + d.ranges = desc_watch.ranges; + desc_merge_ranges(d.ranges); + d.bufs = dw.bufs; + d.imgs = dw.imgs; + d.views = dw.views; + d.colors = desc_watch.colors; + d.images = desc_watch.images; + d.written = desc_watch.written; + d.stamps = desc_watch.stamps; + d.writes.reserve(dw.writes.size()); + for (const VkWriteDescriptorSet& w : dw.writes) { + DescWrite e; + e.head = w; + e.head.pNext = nullptr; + e.head.pBufferInfo = nullptr; + e.head.pImageInfo = nullptr; + e.head.pTexelBufferView = nullptr; + if (w.pBufferInfo) { + const int i = desc_index(w.pBufferInfo, dw.bufs); + if (i < 0) { desc_progs.erase(&p); return; } // (keep the draw's own set; do not reuse the one from before) + e.kind = 1, e.first = (uint32_t)i; + } else if (w.pImageInfo) { + const int i = desc_index(w.pImageInfo, dw.imgs); + if (i < 0) { desc_progs.erase(&p); return; } + e.kind = 2, e.first = (uint32_t)i; + } else if (w.pTexelBufferView) { + const int i = desc_index(w.pTexelBufferView, dw.views); + if (i < 0) { desc_progs.erase(&p); return; } + e.kind = 3, e.first = (uint32_t)i; + } + d.writes.push_back(e); + } + for (const Stage& st : p.stages) { + if (st.nvn_stage == 5) d.grid_matters = true; + if (st.nvn_stage != 5 && st.nvn_stage != 6 && st.nvn_stage != 7) continue; + const PointerEntry* e = pointer_table(s, st.nvn_stage); + DescProg::Ptr ps; + ps.stage = st.nvn_stage; + if (e) std::memcpy(ps.e, e, sizeof ps.e); + d.ptrs.push_back(ps); + } + if (d.grid_matters) std::memcpy(d.grid, s.grid, sizeof d.grid); + desc_progs[&p] = std::move(d); +} +bool desc_prog_use(const Program& p, const NvnState& s, uint64_t reg) { + auto it = desc_progs.find(&p); + if (it == desc_progs.end() || !it->second.on) return false; + DescProg& d = it->second; + if (d.desc_gen != s.desc_gen || d.reg_gen != reg || d.stamp != gpu_bind_stamp() || + d.destroy_gen != texture_destroy_generation()) + return false; + if (d.grid_matters && std::memcmp(d.grid, s.grid, sizeof d.grid)) return false; + const size_t lim = ub_update_count(); + if (d.any_update_misses) { + if (lim != d.ub_mark) return false; + } else if (!d.ranges.empty()) { + static thread_local std::vector addr, len; + addr.clear(), len.clear(); + addr.reserve(d.ranges.size()), len.reserve(d.ranges.size()); + for (const auto& rg : d.ranges) addr.push_back(rg.first), len.push_back(rg.second); + if (ub_since_miss(d.ub_mark, addr.data(), len.data(), (int)addr.size())) return false; + } else if (ub_since_miss(d.ub_mark, nullptr, nullptr, 0)) { + return false; + } + if (memory_generation() != d.mem_gen) + for (const auto& rg : d.ranges) { + if (!rg.second) continue; + const uint64_t end = rg.first + rg.second; + // (page stamps cover 512 GB. A GPU address past that is not a guest page, so a write there would not show) + if (end < rg.first || end > (1ull << 39) || memory_written_since(rg.first, rg.second, d.mem_gen)) + return false; + } + for (const DescProg::Ptr& ps : d.ptrs) { + const PointerEntry* e = pointer_table(s, ps.stage); + if (!e || std::memcmp(e, ps.e, sizeof ps.e)) return false; + } + for (const DescWrite& e : d.writes) { + if (e.kind == 1 && e.first > d.bufs.size()) return false; + if (e.kind == 2 && e.first > d.imgs.size()) return false; + if (e.kind == 3 && e.first > d.views.size()) return false; + } + dw.bufs = d.bufs; + dw.imgs = d.imgs; + dw.views = d.views; + dw.writes.clear(); + dw.writes.reserve(d.writes.size()); + for (const DescWrite& e : d.writes) { + VkWriteDescriptorSet w = e.head; + if (e.kind == 1) w.pBufferInfo = dw.bufs.empty() ? nullptr : dw.bufs.data() + e.first; + else if (e.kind == 2) w.pImageInfo = dw.imgs.empty() ? nullptr : dw.imgs.data() + e.first; + else if (e.kind == 3) w.pTexelBufferView = dw.views.empty() ? nullptr : dw.views.data() + e.first; + dw.writes.push_back(w); + } + draw_sampled.clear(); + for (const Texture* t : d.colors) + if (t && t->gpu_written) draw_sampled.push_back(t); + for (Texture* t : d.images) + if (t) draw_sampled.push_back(t); + for (Texture* t : d.written) + if (t) { + ParLock lk; + texture_written(t); + } + for (const auto& st : d.stamps) + if (st.first && st.second) { + ParLock lk; + memory_stamp_gpu(st.first, st.second); + } + if (!d.any_update_misses) d.ub_mark = lim; + st_desc_prog++; + return true; +} +void desc_prog_build(const Program& p, const NvnState& s, uint64_t reg, bool keep) { + const uint64_t stamp = gpu_bind_stamp(), destroy_gen = texture_destroy_generation(), desc = s.desc_gen; + const size_t mark = ub_update_count(); + struct Off { + ~Off() { desc_watch_on = false; } + } off; + if (keep) desc_watch_begin(); + build_descriptors(s, p); + if (keep && stamp == gpu_bind_stamp() && destroy_gen == texture_destroy_generation() && desc == s.desc_gen && + reg == registrations_generation()) + desc_prog_keep(p, s, reg, stamp, destroy_gen, mark); +} + /// Binds the descriptors built: pushed, or a set from the recording's pool // (push descriptors keep what is not pushed again: a program drawn again pushes only the bindings that changed, // while nothing else was bound at its bind point in this command buffer) @@ -2756,6 +2995,7 @@ void draw_begin_recording(int slot, VkCommandBuffer cmd, uint64_t value) { pass = Pass{}; work_since_barrier = false; last_dyn_valid = false, last_desc_valid = false, last_vb_valid = false, last_pipeline = VK_NULL_HANDLE; + desc_progs.clear(); descriptors_disturbed(); static thread_local uint64_t hits_value = ~0ull; // (resolved targets last a submission, through its parts: no CPU write is if (value != hits_value) { // collected between them; resolved handles last while their slot and @@ -4044,21 +4284,24 @@ void draw(VkCommandBuffer cmd, NvnState& s, const DrawArgs& d) { const bool same_desc = !no_reuse && last_desc_valid && last_desc_prog == &p && last_desc_bind == s.bind_gen && last_desc_reg == reg_gen && last_desc_ub == ub_gen; if (!same_desc) { - PROF(PROF_DESC); trace_tex_hit = nullptr, trace_tex_mask = 0; - build_descriptors(s, p); // (textures first used here are uploaded by the recording's upload command buffer) - static std::string last_logged; - if (dump_on()) { - char head[256]; - int hn = snprintf(head, sizeof head, "prog %016llx %ux%u:", (unsigned long long)p.key, at.width, at.height); - for (int i = 0; i < at.n && hn < 200; i++) // (the targets' NVN formats and the views' Vulkan ones) - if (at.color[i]) - hn += snprintf(head + hn, sizeof head - hn, " c%d:#%u/n%#x/v%d", i, at.color[i]->id, - s.rt.has_cview[i] && s.rt.cview[i].set_format ? (int)s.rt.cview[i].format - : (int)s.rt.color[i]->d.format, - (int)at.color_format[i]); - if (last_logged != head + dump_inputs) - last_logged = head + dump_inputs, logf("vk: dump-in %03d draw %s", dump_seq(), last_logged.c_str()); + const bool quiet = dump_on() || probe_on() || stats_frame_now(); + if (no_reuse || quiet || !desc_prog_use(p, s, reg_gen)) { + PROF(PROF_DESC); + desc_prog_build(p, s, reg_gen, !no_reuse && !quiet); // (textures first used here are uploaded) + static std::string last_logged; + if (dump_on()) { + char head[256]; + int hn = snprintf(head, sizeof head, "prog %016llx %ux%u:", (unsigned long long)p.key, at.width, at.height); + for (int i = 0; i < at.n && hn < 200; i++) // (the targets' NVN formats and the views' Vulkan ones) + if (at.color[i]) + hn += snprintf(head + hn, sizeof head - hn, " c%d:#%u/n%#x/v%d", i, at.color[i]->id, + s.rt.has_cview[i] && s.rt.cview[i].set_format ? (int)s.rt.cview[i].format + : (int)s.rt.color[i]->d.format, + (int)at.color_format[i]); + if (last_logged != head + dump_inputs) + last_logged = head + dump_inputs, logf("vk: dump-in %03d draw %s", dump_seq(), last_logged.c_str()); + } } } if (dump_on()) log_dump_draw(s, p, d); @@ -5023,7 +5266,13 @@ void dispatch(VkCommandBuffer cmd, NvnState& s, uint32_t x, uint32_t y, uint32_t if (plk.on) par_mutex().unlock(), plk.on = false; dclock.t_pipe = slow_dispatch_timing() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; end_pass(cmd); - build_descriptors(s, p); + { + const uint64_t reg_gen = registrations_generation(); + static const bool no_reuse = getenv("RX_VK_NO_DESC_REUSE") != nullptr; + const bool quiet = dump_on() || probe_on() || stats_frame_now(); + if (no_reuse || quiet || !desc_prog_use(p, s, reg_gen)) + desc_prog_build(p, s, reg_gen, !no_reuse && !quiet); + } dclock.t_desc = slow_dispatch_timing() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; if (dump_on()) logf("vk: dump-in %03d dispatch %ux%ux%u prog %016llx:%s", dump_seq(), x, y, z, (unsigned long long)p.key, @@ -5284,6 +5533,9 @@ void draw_records_log(uint64_t lo, uint64_t hi) { int64_t draw_desc_reused() { return st_desc_reused; } +int64_t draw_desc_prog_reused() { + return st_desc_prog; +} void pipeline_cache_save(bool force) { if (!pcache || (!force && pcache_built_since_save < 32)) return; diff --git a/gpu/vk/vk_draw.h b/gpu/vk/vk_draw.h index 1c224c0d2..1fcca86d8 100644 --- a/gpu/vk/vk_draw.h +++ b/gpu/vk/vk_draw.h @@ -50,11 +50,24 @@ void draw_stats2(int64_t* passes, int64_t* mesh, double* pipeline_ms); void draw_stats3(int64_t* pending_skips, int64_t* waits, double* wait_ms); void draw_skip_report(double frames); // (hookshot) skips by the line that made them int64_t draw_desc_reused(); +int64_t draw_desc_prog_reused(); /// RX_VK_FLASH_RECORD=,...: the last frames' draws of those programs, written to the log for frames around a flash void draw_records_log(uint64_t frame_lo, uint64_t frame_hi); void pass_traces_log(uint64_t frame_lo, uint64_t frame_hi); // (RX_VK_PASS_TRACE=1) /// Changes with every uniform update the stream makes (vk_replay.cpp): descriptors bound before may be stale uint64_t ub_generation(); +/// Uniform updates this replay place can see (a worker: those before its segment) +size_t ub_update_count(); +/// True when an update at or after mark overlaps a range, or the list shrank below mark. No ranges: false. +bool ub_since_miss(size_t mark, const uint64_t* addr, const uint64_t* len, int count); +/// True when a range of the first list overlaps a range of the second. Empty overlaps nothing. A wrapping length overlaps. +bool range_lists_overlap(const uint64_t* a, const uint64_t* an, int na, const uint64_t* b, const uint64_t* bn, int nb); +/// Set while a descriptor set is built, so the uniform ranges that build reads are remembered (off: one load) +extern thread_local bool desc_watch_on; +void desc_note_range_slow(uint64_t addr, uint64_t n); +inline void desc_note_range(uint64_t addr, uint64_t n) { + if (desc_watch_on && n) desc_note_range_slow(addr, n); +} /// The pipeline cache to disk (force: whatever was built since the last save; else only after a few new ones) void pipeline_cache_save(bool force); diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 59a6eeb6b..762c51ca8 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -308,7 +308,7 @@ void use_queue_state(uint64_t queue) { cur_state = s; static uint64_t gen_base = 0; gen_base += 1ull << 32; - s->bind_gen = gen_base, s->rt_serial = gen_base; + s->bind_gen = gen_base, s->desc_gen = gen_base, s->rt_serial = gen_base; } template @@ -431,6 +431,16 @@ bool ub_gap(const uint64_t* dst, const uint64_t* end, int n, uint64_t addr, uint *hi = right; return true; } +bool range_lists_overlap(const uint64_t* a, const uint64_t* an, int na, const uint64_t* b, const uint64_t* bn, int nb) { + for (int i = 0; i < na; i++) { + if (!an[i] || a[i] + an[i] < a[i]) return true; + for (int j = 0; j < nb; j++) { + if (!bn[j] || b[j] + bn[j] < b[j]) return true; + if (a[i] < b[j] + bn[j] && b[j] < a[i] + an[i]) return true; + } + } + return false; +} struct UbGapCache { uint64_t epoch = 0, lo = 0, hi = 0; size_t first = 0, lim = 0; @@ -504,6 +514,7 @@ void ub_copy_keep(uint64_t addr, uint64_t n, const uint8_t* base, size_t first, c.rec = rec_value_now, c.view = view, c.epoch = epoch; } uint64_t ub_overlay(uint64_t addr, uint64_t n, uint8_t* base, const std::function& alloc) { + desc_note_range(addr, n); const size_t lim = std::min(ub_limit, ub_updates.size()); // (a worker: those before its place in the stream) static const bool cur_only = getenv("RX_VK_UB_CUR_ONLY") != nullptr; // (A/B: this submission's updates only) const size_t first = cur_only ? std::min(ub_first_of_recording, lim) : 0; @@ -540,6 +551,7 @@ uint64_t ub_overlay(uint64_t addr, uint64_t n, uint8_t* base, const std::functio /// the stream made before this place (a worker: before its segment) over it. For what the replay reads on the CPU for /// the GPU's sake (handles in uniform and storage buffers): the game's memory now may be its next frames'. bool ub_read(uint64_t addr, void* dst, uint64_t n) { + desc_note_range(addr, n); const size_t lim = std::min(ub_limit, ub_updates.size()); if (n == 4) { uint32_t v = 0; @@ -570,6 +582,20 @@ bool ub_read(uint64_t addr, void* dst, uint64_t n) { return true; } +size_t ub_update_count() { + return std::min(ub_limit, ub_updates.size()); +} +bool ub_since_miss(size_t mark, const uint64_t* addr, const uint64_t* len, int count) { + const size_t lim = std::min(ub_limit, ub_updates.size()); + if (lim < mark || mark > ub_updates.size()) return true; + if (mark == lim || count <= 0) return false; + for (size_t i = mark; i < lim; i++) { + const uint64_t u = ub_updates[i].dst, un = ub_updates[i].bytes.size(); + if (range_lists_overlap(&u, &un, 1, addr, len, count)) return true; + } + return false; +} + namespace { /// The recording's uniform updates into guest memory (after its draws: the ones that came after an update read @@ -825,7 +851,7 @@ void replay(VkCommandBuffer& cmd, NvnState& s, const uint8_t* p, const uint8_t* const auto b = payload(a, avail); // (the same program bound again: its descriptors already stand) const bool same = b.prog && s.has_prog ? std::memcmp(&s.prog, b.prog, sizeof s.prog) == 0 : !b.prog && !s.has_prog; - if (!same) s.bind_gen++; + if (!same) s.bind_gen++; // (another program: desc_gen stays, so the set built for this one can be pushed again) if (b.prog) s.prog = *b.prog; s.has_prog = b.prog != nullptr && b.prog->nstage > 0; break; @@ -903,7 +929,7 @@ void replay(VkCommandBuffer& cmd, NvnState& s, const uint8_t* p, const uint8_t* } nvn_p_bind_buf& u = (b.ssbo ? s.ssbo : s.ubo)[b.stage][index]; if (!buf_unchanged(u.stage, u.index, u.addr, u.size, b.stage, index, b.r[i].address, b.r[i].size)) - s.bind_gen++; + s.bind_gen++, s.desc_gen++; u.stage = b.stage, u.index = index, u.addr = b.r[i].address, u.size = b.r[i].size; } break; @@ -920,7 +946,8 @@ void replay(VkCommandBuffer& cmd, NvnState& s, const uint8_t* p, const uint8_t* } if (b.stage >= 0 && b.stage < kNvnStages && b.index >= 0 && b.index < 16) { nvn_p_bind_buf& u = (h.op == NOP_BIND_UBO ? s.ubo : s.ssbo)[b.stage][b.index]; - if (!buf_unchanged(u.stage, u.index, u.addr, u.size, b.stage, b.index, b.addr, b.size)) s.bind_gen++; + if (!buf_unchanged(u.stage, u.index, u.addr, u.size, b.stage, b.index, b.addr, b.size)) + s.bind_gen++, s.desc_gen++; u = b; } break; @@ -940,7 +967,7 @@ void replay(VkCommandBuffer& cmd, NvnState& s, const uint8_t* p, const uint8_t* const int lim = h.op == NOP_BIND_IMG ? 32 : 128; int n = 0; while (n < b.n && b.first + n < lim) n++; - if (!handles_unchanged(to + b.first, b.h, n)) s.bind_gen++; // (the same handles: the draw keeps its descriptors) + if (!handles_unchanged(to + b.first, b.h, n)) s.bind_gen++, s.desc_gen++; // (the same handles: the draw keeps its descriptors) for (int k = 0; k < n; k++) to[b.first + k] = b.h[k]; break; } @@ -997,7 +1024,7 @@ void replay(VkCommandBuffer& cmd, NvnState& s, const uint8_t* p, const uint8_t* s.render_enabled ? "on" : "off"); break; } - case NOP_SET_TEX_POOL: case NOP_SET_SAMP_POOL: s.bind_gen++; break; + case NOP_SET_TEX_POOL: case NOP_SET_SAMP_POOL: s.bind_gen++, s.desc_gen++; break; case NOP_COPY_STRIDE: s.copy_stride = payload(a, avail).stride; break; case NOP_PUSH_CONST: { // (hookshot) compute push constants, kept in the state (vk_bank.cpp serves them from c0) const auto u = payload(a, avail); @@ -1006,7 +1033,7 @@ void replay(VkCommandBuffer& cmd, NvnState& s, const uint8_t* p, const uint8_t* const bool same = (uint64_t)u.offset + u.size <= s.cs_push_n && std::memcmp(s.cs_push + u.offset, src, u.size) == 0; std::memcpy(s.cs_push + u.offset, src, u.size); s.cs_push_n = std::max(s.cs_push_n, u.offset + u.size); - if (!same) s.bind_gen++; + if (!same) s.bind_gen++, s.desc_gen++; } break; } @@ -2183,9 +2210,11 @@ void log_frame_stats() { draw_stats3(&pskip, &pwaits, &pwait_ms); static int64_t l_pskip, l_pwaits; static double l_pwait_ms; - static int64_t l_reused; + static int64_t l_reused, l_prog; logf("vk: a frame: %.0f draws kept their neighbour's descriptors", (draw_desc_reused() - l_reused) / 120.0); l_reused = draw_desc_reused(); + logf("vk: a frame: %.0f draws reused a program's descriptors", (draw_desc_prog_reused() - l_prog) / 120.0); + l_prog = draw_desc_prog_reused(); logf("vk: a frame: %.1f draws left out (not ready), %.2f waits for shaders or pipelines (%.1f ms); " "translation %.0f ms in all, %d jobs queued", (pskip - l_pskip) / 120.0, (pwaits - l_pwaits) / 120.0, (pwait_ms - l_pwait_ms) / 120.0, diff --git a/gpu/vk/vk_state.h b/gpu/vk/vk_state.h index c1e84d064..a10ba34c1 100644 --- a/gpu/vk/vk_state.h +++ b/gpu/vk/vk_state.h @@ -29,6 +29,7 @@ struct NvnState { nvn_p_set_rt rt{}; uint64_t rt_serial = 0; // bumped at each NOP_SET_RT (the draw path resolves the targets once per change) uint64_t bind_gen = 0; // bumped when a binding the descriptors read changes (a rebind of the same values does not) + uint64_t desc_gen = 0; // the same, except a program change: that program's descriptors stay usable float viewport[16][4]{}; float depth_near = 0.0f, depth_far = 1.0f; int32_t scissor[4]{0, 0, 0, 0}; diff --git a/tests/test_desc_prog.py b/tests/test_desc_prog.py new file mode 100644 index 000000000..c8ebacd1d --- /dev/null +++ b/tests/test_desc_prog.py @@ -0,0 +1,111 @@ +"""A program's descriptors are pushed again when that program returns. + +Binding another program used to drop the set, so the next draw of the first one built it +again. python -I -B tests/test_desc_prog.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 ordered(text, early, late, what): + if text.index(early) > text.index(late): + raise SystemExit(what) + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--cxx", default="g++") + a = p.parse_args() + replay = (ROOT / "gpu" / "vk" / "vk_replay.cpp").read_text(encoding="utf-8") + draw = (ROOT / "gpu" / "vk" / "vk_draw.cpp").read_text(encoding="utf-8") + state = (ROOT / "gpu" / "vk" / "vk_state.h").read_text(encoding="utf-8") + if "uint64_t desc_gen" not in state: + raise SystemExit("descriptors have no generation of their own") + program = block(replay, "case NOP_BIND_PROGRAM:") + if "s.bind_gen++" not in program or "s.desc_gen++" in program: + raise SystemExit("a program change must leave that program's descriptors usable") + bumps = [ln for ln in replay.splitlines() if "s.bind_gen++" in ln] + if len(bumps) != 6 or sum("s.desc_gen++" not in ln for ln in bumps) != 1: + raise SystemExit("every binding except the program must drop the kept descriptors") + if "s->desc_gen = gen_base" not in block(replay, "void use_queue_state("): + raise SystemExit("a queue switch keeps another queue's descriptors") + if replay.count("desc_progs.clear(") + draw.count("desc_progs.clear(") != 1: + raise SystemExit("the kept descriptors must be dropped exactly when a recording starts") + if "desc_progs.clear()" not in block(draw, "void draw_begin_recording("): + raise SystemExit("a new recording keeps descriptors whose stream was reset") + if "desc_progs.clear()" in block(draw, "void clear_color_target("): + raise SystemExit("a masked clear drops descriptors that only need pushing again") + watch = draw.index("thread_local bool desc_watch_on") + anon = draw.index("\nnamespace {\n", draw.index("namespace mkwvk")) + if watch > anon or draw.count("thread_local bool desc_watch_on") != 1: + raise SystemExit("the uniform watch is hidden from the replay") + for name in ("bool ub_read(", "uint64_t ub_overlay("): + fn = block(replay, name) + ordered(fn, "desc_note_range(", "const size_t lim", f"{name} notes its range too late") + body = block(draw, "void draw(") + ordered(body, "desc_prog_use(", "desc_prog_build(", "a draw builds descriptors before looking for the program's set") + disp = block(draw, "void dispatch(") + ordered(disp, "desc_prog_use(", "desc_prog_build(", "a dispatch builds descriptors before looking for the program's set") + built = block(draw, "void desc_prog_build(") + if "build_descriptors(" not in built: + raise SystemExit("a miss does not build the set") + used = block(draw, "bool desc_prog_use(") + if "pointer_table(" not in used or "1ull << 39" not in used: + raise SystemExit("a kept set ignores a pointer table or a write past the page table") + if "memory_stamp_gpu(" not in used or "texture_written(" not in used: + raise SystemExit("reusing a set skips a storage write the build would have recorded") + fn = block(replay, "bool range_lists_overlap(") + 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 a[] = {{0, 100}}; + const uint64_t an[] = {{10, 5}}; + const uint64_t b[] = {{20}}; + const uint64_t bn[] = {{5}}; + CHECK(!range_lists_overlap(a, an, 1, b, bn, 1)); + const uint64_t over[] = {{8}}; + CHECK(range_lists_overlap(a, an, 1, over, bn, 1)); + const uint64_t abut[] = {{10}}; + CHECK(!range_lists_overlap(a, an, 1, abut, bn, 1)); + CHECK(!range_lists_overlap(a, an, 0, b, bn, 1)); + CHECK(!range_lists_overlap(a, an, 1, b, bn, 0)); + const uint64_t zero[] = {{0}}; + CHECK(range_lists_overlap(a, zero, 1, b, bn, 1)); + const uint64_t wrap[] = {{~uint64_t{{0}} - 2}}; + const uint64_t wn[] = {{8}}; + CHECK(range_lists_overlap(wrap, wn, 1, b, bn, 1)); + const uint64_t inner[] = {{102}}; + const uint64_t inn[] = {{2}}; + CHECK(range_lists_overlap(a, an, 2, inner, inn, 1)); + const uint64_t beside[] = {{106}}; + CHECK(!range_lists_overlap(a, an, 2, beside, bn, 1)); + std::printf("PASS: a uniform update drops a kept set only when it overlaps\\n"); +}} +''' + with tempfile.TemporaryDirectory(prefix="desc-prog-") 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()