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