Fix NVN2 compute constants, indirect mesh draws and empty descriptor sets, and cut the texture pool refresh.

NVN2 compute banks get the dispatch's group counts in bytes 0-0xb, for direct and indirect dispatches; the fog pass
after the integration divided by them. Indirect mesh draws read NVN's {count, first} record as {x, y, z}, so y = 0
drew nothing: each now draws from a ring entry holding {count, 1, 1}, copied on the GPU before the draw. An empty set 1
no longer leaves set 0 pushed with 0 bytes. Compute stages with subgroup operations whose width is a whole number of
subgroups require full subgroups.

The texture pool refresh fell from 3.1-3.7 ms to 1.0 ms a frame in Kokiri Forest: slots with the same descriptor,
address generation and entry generation share one lookup (about 14,500 of 15,000 refills a frame), and registering a
texture and view identical to what the slot holds keeps its resolution. Under 20 GiB of VRAM, ASTC textures are
encoded to BC3 by default (Kokiri: 15.9 to 8.2 GiB, same GPU time). The pool log counts refills by cause and what
invalidates them; frame dumps write BC textures as raw blocks and print a storage buffer's backing span; a profile with
NX2NAVI_PROFILE_FRAMES is written when its window ends; the alpha test logs once that it is not applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-06 21:44:21 -05:00
parent 85f401669f
commit c7257fe652
8 changed files with 223 additions and 19 deletions

View File

@@ -291,6 +291,11 @@ NVN_API(nvnColorStateSetAlphaTest) {
unsigned f = (unsigned)arg_x(c, 1);
if (f > 15) rt_fatal("nvn: alpha func %u", f);
ST(color_st)->alpha_func = f;
// the replay does not apply the test (vk_pipeline.c): say once that the game asks for one
if (f && f != NVN_ALPHA_FUNC_ALWAYS) {
static int once_;
if (!once_++) rt_log("nvn: SetAlphaTest %u: the alpha test is not applied", f);
}
}
// NVNboolean nvnColorStateGetBlendEnable(const NVNcolorState *s, int index)
NVN_API(nvnColorStateGetBlendEnable) {
@@ -395,7 +400,10 @@ NVN_API(nvnCommandBufferSetSampleMask) {
}
// void nvnCommandBufferSetAlphaRef(NVNcommandBuffer *cmd, float ref): used only with the alpha test, which the replay
// reports
NVN_API(nvnCommandBufferSetAlphaRef) { (void)c; }
NVN_API(nvnCommandBufferSetAlphaRef) {
static int once_;
if (!once_++) rt_log("nvn: SetAlphaRef %g: the alpha test is not applied", (double)arg_s(c, 0));
}
// void nvnCommandBufferSetDepthBounds(NVNcommandBuffer *cmd, NVNboolean enable, float min, float max): disabling is
// exact; an enabled range is drawn without the test and reported
NVN_API(nvnCommandBufferSetDepthBounds) {

View File

@@ -363,6 +363,9 @@ int prof_sampler_start(void) {
// dump of a present at least slow_ns after the previous one (d 0 on the first present).
void prof_sampler_frame(uint64_t frame, uint64_t now, uint64_t d) {
cur_frame = frame;
// the end of a NX2NAVI_PROFILE_FRAMES window writes the profile then: a test run ended by a timeout never reaches
// the exit handlers (prof_write)
if (frame_hi && frame > frame_hi && !written) prof_sampler_write();
if (slow_ns && d >= slow_ns && !dump_hi) {
dump_lo = (LONG64)(now - d - 20000000ull);
dump_frame = (LONG64)frame;

View File

@@ -429,6 +429,10 @@ VkImage vk_tonemap_record(VkCommandBuffer cb, VkImage src, VkFormat sf, uint32_t
// its storage address is created, dropped, released or taken back into use. Unlocked; read it before the lookup it
// guards (a lookup may create an entry). 0 when the slot was never registered: nothing is kept under 0.
uint64_t vk_texture_slot_token(int id);
uint64_t vk_texture_same_regs(void);
void vk_texture_bucket_stats(char *out, size_t n); // diagnosis for the pool log (vk_texture.c)
uint64_t vk_texture_slot_khash_locked(int id); // vk_texture.c: descriptor hash of a GPU-written slot, 0 otherwise
uint32_t vk_texture_entry_gen(void); // vk_texture.c: bumped by every entry change // registrations that changed nothing (vk_texture_register), for the pool log
// Slots whose token may have changed since *pos (0 at first; then the value these calls leave): fn is called for each
// (some more than once) and *pos advanced. 0 when too many changes came since: the caller looks at every slot. *pos
// from vk_texture_dirty_pos_locked after such a walk. Texture lock held.

View File

@@ -301,16 +301,17 @@ static int create_pipeline(VkCtx *c) {
}
// NX2NAVI_ASTC_BC=1: the BC3 encoder, when the device samples BC3 (every desktop GPU does); 0 leaves ASTC as RGBA8.
// Unset: on when the largest device-local heap is under 5 GiB. There RGBA8 ASTC, four times the size of BC3, fills
// video memory (a GTX 1650 4 GiB went from 10-23 to 3-12 ms of GPU time a frame, issue #87); BC3 loses some precision
// (normal maps), so GPUs with more memory keep RGBA8.
// Unset: on when the largest device-local heap is under 20 GiB. There RGBA8 ASTC, four times the size of BC3, fills
// video memory (a GTX 1650 4 GiB went from 10-23 to 3-12 ms of GPU time a frame, issue #87). Ocarina in Kokiri Forest
// holds 15.9 GiB of VRAM with RGBA8 and 8.2 GiB with BC3, at the same GPU time and no difference to see in the frame,
// so 8-16 GiB cards take BC3 too; BC3 loses some precision (normal maps), so 24 and 32 GiB cards keep RGBA8.
static int create_bc_pipeline(VkCtx *c) {
const char *e = cfg_get("NX2NAVI_ASTC_BC");
VkDeviceSize vram = 0;
for (uint32_t h = 0; h < c->mem_props.memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
if (c->mem_props.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT && c->mem_props.memoryHeaps[h].size > vram)
vram = c->mem_props.memoryHeaps[h].size;
int on = e && *e ? *e != '0' : vram < (5ull << 30);
int on = e && *e ? *e != '0' : vram < (20ull << 30);
if (!(e && *e))
rt_log("vk: NX2NAVI_ASTC_BC unset: BC3 %s (largest device-local heap %llu MiB)", on ? "on" : "off",
(unsigned long long)(vram >> 20));

View File

@@ -286,6 +286,15 @@ static void job_done(void *arg) {
VmaAllocator vma = vk_ctx()->vma;
vmaInvalidateAllocation(vma, j->alloc, 0, VK_WHOLE_SIZE);
write_images(j);
// A BC texture's raw blocks also go to <name>.bin, packed rows of blocks, so a script can decode them: the PNG
// shows the block words through the integer mapping, which loses their bits.
size_t nl = strlen(j->name);
if (nl > 7 && !strcmp(j->name + nl - 7, "_blocks")) {
char path[600];
snprintf(path, sizeof path, "%s/%s.bin", dir, j->name);
FILE *f = fopen(path, "wb");
if (f) fwrite(j->map, 1, j->bytes, f), fclose(f);
}
vmaDestroyBuffer(vma, j->buf, j->alloc);
plat_add64(&inflight, -(int64_t)j->bytes);
free(j);
@@ -582,7 +591,12 @@ static void describe_bindings(Str *b, const VkExec *ex, const VkPipeInfo *info)
memcpy(&pa, ex->cs_push + r->handle * 4 - 0x10, 8), memcpy(&ps, ex->cs_push + r->handle * 4 - 0x10 + 8, 4);
sp(b, " (push pointer %#llx+%#x)", (unsigned long long)pa, ps);
}
if (st == 5) sp(b, " -> %#llx", (unsigned long long)vk_exec_cs_storage_addr(ex, r));
if (st == 5) { // the address and how far its backing span reaches (what a size-0 binding covers)
uint64_t ca = vk_exec_cs_storage_addr(ex, r);
VkMemSpan ms;
sp(b, " -> %#llx", (unsigned long long)ca);
if (ca && vk_mem_resolve(ca, 1, &ms)) sp(b, " (span %#llx)", (unsigned long long)ms.avail);
}
for (int k = 0; k < 16; k++)
if (ex->ssbo[st][k].addr) {
VkDescriptorBufferInfo cb; // "(copy)": a binding that only reads it uses the submit-time copy (vk_usnap.c)

View File

@@ -1049,6 +1049,10 @@ static VkDescriptorBufferInfo c0_info(VkExec *ex, int st) {
nvn2_cs_tables(pctl, &nu, &ns);
uint32_t pn = ta - 0x10 < sizeof ex->cs_push ? ta - 0x10 : sizeof ex->cs_push;
memcpy(tmp + 0x10, ex->cs_push, pn < VK_C0_BYTES - 0x10 ? pn : VK_C0_BYTES - 0x10);
// Bytes 0-0xb of an NVN2 compute bank are the dispatch's group counts (x, y, z). The fog pass after the
// integration (passes 0115/0116 of a Kokiri frame) maps each thread to tid / (groups * 16 - 1): with zeros
// there every thread read the same point. An indirect dispatch leaves them 0 (op_dispatch_indirect).
memcpy(tmp, c0_dim, 12);
for (uint32_t k = 0; k < nu + ns && ta + 16 * k + 12 <= VK_C0_BYTES; k++) {
const p_bind_buf *sb = k < nu ? (k < 18 ? &ex->ubo[st][k] : NULL) : (k - nu < 16 ? &ex->ssbo[st][k - nu] : NULL);
if (!sb) continue;
@@ -1603,6 +1607,7 @@ static struct {
uint64_t logn; // writes so far
uint64_t *last; // per entry: the log position of its newest write (catch-up writes it once, there)
uint64_t switches, waits, catchup, written, rewrites, catch_ns, sync_ns, n_slots, n_refill, n_fresh;
uint64_t n_by_reg, n_by_addr; // refills whose token changed by a registration, by an address generation only
uint64_t dirty_pos; // vk_texture_dirty_slots_locked position (0: the next sync walks every slot)
int ready, uab;
uint32_t kinds, gen, epoch, sgen;
@@ -1755,6 +1760,8 @@ static int pool_take_set(VkExec *ex, uint64_t *from) {
}
// The current set gets the entries written into other sets in [from, to) of the log, as PL.view / PL.samp hold them now.
static uint64_t pool_memo_hits; // pool_fill: slots that shared another slot's lookup (defined with pool_memo below)
static void pool_catch_up(uint64_t from, uint64_t to) {
VkDescriptorSet set = PL.set;
if (to - from > POOL_LOG) { // the log has wrapped since: every entry
@@ -1771,15 +1778,22 @@ static void pool_catch_up(uint64_t from, uint64_t to) {
}
pool_flush();
PL.switches++;
if (PL.switches % 3600 == 0) { // diagnosis: what invalidates the slots (vk_texture.c vk_texture_bucket_stats)
char bs[320];
vk_texture_bucket_stats(bs, sizeof bs);
rt_log("vk: texture pool buckets over 3600 switches: %s", bs);
}
if (PL.switches % 3600 == 0)
rt_log("vk: texture pool sets: %llu switches, %llu waited for the GPU, %.1f log entries and %.1f writes per "
"catch-up, %llu whole rewrites; %.3f ms catch-up and %.3f ms entry refresh per switch (%.0f slots, %.0f "
"refilled, %.0f checked)",
"refilled: %.0f registered, %.0f address changes, %.0f shared a lookup; %.0f checked); %llu identical registrations kept",
(unsigned long long)PL.switches, (unsigned long long)PL.waits, (double)PL.catchup / (double)PL.switches,
(double)PL.written / (double)PL.switches, (unsigned long long)PL.rewrites,
PL.catch_ns / 1e6 / (double)PL.switches, PL.sync_ns / 1e6 / (double)PL.switches,
(double)PL.n_slots / (double)PL.switches, (double)PL.n_refill / (double)PL.switches,
(double)PL.n_fresh / (double)PL.switches);
(double)PL.n_by_reg / (double)PL.switches, (double)PL.n_by_addr / (double)PL.switches,
(double)pool_memo_hits / (double)PL.switches,
(double)PL.n_fresh / (double)PL.switches, (unsigned long long)vk_texture_same_regs());
}
// Entry id of every kind in mask: the slot's view of that kind or the kind's stand-in. Texture lock held. 0: the
@@ -1821,8 +1835,28 @@ static int pool_on(void) {
return on;
}
// Slots that resolve alike share one lookup (vk_texture_slot_khash_locked): Ocarina keeps ~400 descriptors of each
// transient render target registered over its storage, and every finalize and initialize of that target changes their
// tokens, so a refill looked up the same texture ~400 times, ~15,000 lookups a frame in Kokiri Forest. An entry holds the
// views one slot got, for the slots with its hash, address generation and entry generation.
#define POOL_MEMO 4096
static struct { uint64_t kh; uint32_t agen, egen, mask; VkImageView view[VK_POOL_KINDS]; } pool_memo[POOL_MEMO];
static uint64_t pool_memo_hits;
static int pool_fill(VkExec *ex, uint32_t id, uint32_t mask, uint64_t tok) {
vk_texture_create_deferred_locked();
uint64_t kh = tok ? vk_texture_slot_khash_locked((int)id) : 0;
uint32_t ag = (uint32_t)tok;
typeof(pool_memo[0]) *m = kh ? &pool_memo[(kh >> 20) % POOL_MEMO] : NULL;
if (m && m->kh == kh && m->agen == ag && m->egen == vk_texture_entry_gen() && (m->mask & mask) == mask) {
for (int k = 0; k < VK_POOL_KINDS; k++) {
if (!(mask >> k & 1)) continue;
VkImageView *e = &PL.view[(size_t)k * VK_POOL_TEX + id];
if (*e != m->view[k]) *e = m->view[k], pool_write((uint32_t)k, id, m->view[k], VK_NULL_HANDLE);
}
pool_memo_hits++;
return 1;
}
for (int k = 0; k < VK_POOL_KINDS; k++) {
if (!(mask >> k & 1)) continue;
GfxShaderRes r = {0};
@@ -1833,7 +1867,15 @@ static int pool_fill(VkExec *ex, uint32_t id, uint32_t mask, uint64_t tok) {
VkImageView *e = &PL.view[(size_t)k * VK_POOL_TEX + id];
if (*e != ii.imageView) *e = ii.imageView, pool_write((uint32_t)k, id, ii.imageView, VK_NULL_HANDLE);
}
return !vk_texture_create_deferred_locked();
int ok = !vk_texture_create_deferred_locked();
if (m && ok) { // the generation after the lookups: an entry they created is part of what they resolved
uint32_t eg = vk_texture_entry_gen();
if (m->kh != kh || m->agen != ag || m->egen != eg) m->kh = kh, m->agen = ag, m->egen = eg, m->mask = 0;
for (int k = 0; k < VK_POOL_KINDS; k++)
if (mask >> k & 1) m->view[k] = PL.view[(size_t)k * VK_POOL_TEX + id];
m->mask |= mask;
}
return ok;
}
static void pool_sync_entries(VkExec *ex);
@@ -1863,6 +1905,8 @@ static void pool_dirty_slot(int id, void *arg) { // vk_texture_dirty_slots_loc
uint64_t t = vk_texture_slot_token(id);
if (t == PL.tok[id]) return;
PL.n_refill++;
if (t >> 32 != PL.tok[id] >> 32) PL.n_by_reg++;
else PL.n_by_addr++;
if (pool_fill(d->ex, (uint32_t)id, d->used, t)) PL.tok[id] = t;
else d->retry = 1;
}
@@ -1897,6 +1941,8 @@ static void pool_sync_entries(VkExec *ex) {
uint64_t t = vk_texture_slot_token(id);
if (t != PL.tok[id]) {
PL.n_refill++;
if (t >> 32 != PL.tok[id] >> 32) PL.n_by_reg++;
else PL.n_by_addr++;
if (pool_fill(ex, (uint32_t)id, used, t)) PL.tok[id] = t;
else retry = 1;
} else if (t && fresh) {
@@ -2502,6 +2548,9 @@ static void bind_resources(VkExec *ex, const VkPipeInfo *info) {
}
nd += cnt;
}
// An empty set 1 (a fragment stage without resources: depth and shadow draws) starts past the last binding, which
// the loop never reaches: d0[1] stayed 0 and set 0 was pushed with 0 bytes, so the driver read stale descriptors
if (n_sets == 2 && lo[1] == hi[1]) d0[1] = nd;
// Under the validation layer, a new graphics layout binds set 1 before set 0 is pushed or bound. The layer (SDK
// 1.4.363, PipelineLayoutCompatDef::operator==) takes any two INDEPENDENT_SETS layouts as compatible, so
// vkCmdPushDescriptorSetKHR keeps the push set it made for an earlier layout and drops writes to bindings that
@@ -3747,10 +3796,43 @@ static void op_draw_mesh(VkExec *ex, const p_draw_mesh *d) {
ex->recorded++;
}
// DrawMeshTasksIndirect: the arguments are written by the GPU earlier in the frame (they never come back to guest
// memory), so the draw reads them from the device copy, taken as VkDrawMeshTasksIndirectCommandEXT {x, y, z}. If the
// game's layout were NV's {count, first} instead, y would be 0 and the draw would draw nothing. One draw; x2-x4 of the
// call vary like leftovers, so they are not read as a count or stride.
// The record NVN's DrawMeshTasksIndirect reads is {count, first} (as NV_mesh_shader's), and Vulkan's {x, y, z}: read as
// it is, y = first (0) drew nothing. Each draw gets an entry of this ring, {count, 1, 1}: y and z filled with 1 and x
// copied from the game's record on the GPU, between the pass's end and the draw (the record is written by the culling
// compute earlier in the frame and never comes back to guest memory). `first` is not applied: the game leaves it 0.
// MESH_RING entries outlast the frames in flight: an entry is rewritten only after a barrier behind its last draw.
enum { MESH_RING = 4096 };
static VkBuffer mesh_ring;
static uint32_t mesh_ring_next;
static int mesh_ring_entry(VkExec *ex, const VkMemSpan *src, VkDeviceSize *at) {
if (!mesh_ring) {
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = 12ull * MESH_RING,
.usage = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
VmaAllocation a;
if (vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &mesh_ring, &a, NULL) != VK_SUCCESS) {
mesh_ring = VK_NULL_HANDLE;
return 0;
}
}
*at = 12ull * (mesh_ring_next++ % MESH_RING);
vk_exec_barrier(ex); // ends the pass (the next draw opens it again) and orders the culling's writes before the copy
vkCmdFillBuffer(ex->cb, mesh_ring, *at + 4, 8, 1);
VkBufferCopy cp = {src->offset, *at, 4};
vkCmdCopyBuffer(ex->cb, src->buffer, mesh_ring, 1, &cp);
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT, .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT,
.dstAccessMask = VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
vkCmdPipelineBarrier2(ex->cb, &di);
ex->recorded += 3;
return 1;
}
// DrawMeshTasksIndirect: one draw from its ring entry (mesh_ring_entry); x2-x4 of the call vary like leftovers, so they
// are not read as a count or stride.
static void op_draw_mesh_ind(VkExec *ex, const p_draw_mesh_ind *d) {
static PFN_vkCmdDrawMeshTasksIndirectEXT fn;
static int looked;
@@ -3768,6 +3850,9 @@ static void op_draw_mesh_ind(VkExec *ex, const p_draw_mesh_ind *d) {
return warn_once(W_INDIRECT, "indirect mesh draw arguments at %#llx are not backed or not aligned: skipped",
(unsigned long long)d->ind);
c0_binst = 0;
VK_MEM_USE(ip);
VkDeviceSize at;
if (!mesh_ring_entry(ex, &ip, &at)) return warn_once(W_MESH, "the indirect mesh ring could not be created: skipped");
if (!prepare_draw_ex(ex, NVN_DRAW_PRIMITIVE_TRIANGLES, 0x7fffffff, 0, 1)) return;
vk_exec_frame.draws++;
static uint32_t n_logged;
@@ -3776,10 +3861,9 @@ static void op_draw_mesh_ind(VkExec *ex, const p_draw_mesh_ind *d) {
rt_log("vk: indirect mesh draw: arguments at %#llx, program %llu", (unsigned long long)d->ind,
(unsigned long long)ex->prog->serial);
}
VK_MEM_USE(ip);
rec_draw_mesh_ind(fn, ex->cb, ip.buffer, ip.offset, 1, 12);
rec_draw_mesh_ind(fn, ex->cb, mesh_ring, at, 1, 12);
ex->recorded++;
vk_mem_count(ip.cls, VK_MUSE_IND, 12);
vk_mem_count(ip.cls, VK_MUSE_IND, 4);
VK_MEM_BOUND(ip);
}
@@ -4206,7 +4290,12 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
bind_resources(ex, info);
twin_dispatch = 0;
if (B.c0_valid[2]) { // the shader binds c0
VkBufferCopy cp = {ip.offset, B.c0_off[2] + 0x200, sizeof(VkDispatchIndirectCommand)};
// NVN2 programs read the group counts from c0 byte 0 (c0_info); at 0x200 their bank holds pushed constants
// or the driver's tables, which the copy would overwrite
const uint8_t *pctl, *pcode;
uint32_t plen;
int nvn2 = find_stage(ex->prog, NVN_STAGE_CS, &pctl, &pcode, &plen) && ctl_nvn2(pctl);
VkBufferCopy cp = {ip.offset, B.c0_off[2] + (nvn2 ? 0 : 0x200), sizeof(VkDispatchIndirectCommand)};
vkCmdCopyBuffer(ex->cb, ip.buffer, B.c0_buf[2], 1, &cp);
ex->recorded++;
vk_exec_barrier(ex);

View File

@@ -79,7 +79,7 @@ static struct {
struct { uint64_t links, wlinks, dlinks, requeued, link_fail, defer, lib_n[4], lib_fail, swaps, slow, pre_used[4]; double link_ms, link_max, lib_ms[4], lib_max[4], dlink_ms, dlink_max; } g;
int vs_outs_all; // NX2NAVI_VS_OUTS: 0 union of paired fragment inputs, 1 (all) all 32 output locations
int dyn_vi, dyn_clamp, dyn_poly, dyn_blend_en, dyn_blend_eq, dyn_wmask, dyn_a2c;
int f_logic_op, f_depth_clamp, f_fill_non_solid, f_dual_src;
int f_logic_op, f_depth_clamp, f_fill_non_solid, f_dual_src, f_full_subgroups;
int pv_last; // provokingVertexLast enabled: NVN's default is the last vertex
VkShaderStageFlags sg32_stages; // stages that accept requiredSubgroupSize = 32 (0: not available)
VkDynamicState dyn[32];
@@ -239,6 +239,7 @@ static int ensure_init(void) {
rt_log("vk_pipeline: subgroup size %u-%u, requiredSubgroupSizeStages %#x, pinning %u on %#x", p13.minSubgroupSize,
p13.maxSubgroupSize, (unsigned)p13.requiredSubgroupSizeStages, vk_caps.sg_size, (unsigned)P.sg32_stages);
}
P.f_full_subgroups = v13.computeFullSubgroups;
P.f_logic_op = f2.features.logicOp;
P.f_depth_clamp = f2.features.depthClamp;
P.f_fill_non_solid = f2.features.fillModeNonSolid;
@@ -877,6 +878,18 @@ static void pin_subgroup32(VkPipelineShaderStageCreateInfo *st, VkPipelineShader
st->pNext = rq;
}
// The x of a compute module's LocalSize (OpExecutionMode 17), 0 when it has none.
static uint32_t local_size_x(const uint32_t *w, uint64_t words) {
for (uint64_t i = 5; w && i < words;) {
uint32_t op = w[i] & 0xffff, n = w[i] >> 16;
if (!n || i + n > words) return 0;
if (op == 16 && n >= 6 && w[i + 2] == 17) return w[i + 3];
if (op == 54) return 0; // OpFunction: the execution modes are all before it
i += n;
}
return 0;
}
// Pool arrays (the translator's bindless texture and sampler arrays of VK_POOL_TEX / VK_POOL_SAMP elements, found by
// their types) move to set 2, at the binding of their view kind (vk.h VK_POOL_KINDS) or VK_POOL_KINDS for samplers;
// the kinds used are noted in vk_pool_kinds_used so the replay fills them. NULL: nothing to move (or no memory).
@@ -2692,6 +2705,12 @@ static VkPipeline compute_create(const GfxShader *cs, const Layout *l) {
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
.layout = l->layout};
pin_subgroup32(&ci.stage, &rqc, code, words);
// Subgroup operations in a workgroup whose width is a whole number of subgroups need every subgroup full: the
// driver may otherwise run partial ones, and a ballot over the culling's candidates then counted differently from
// one frame to the next
uint32_t lx = local_size_x(code, words);
if (P.f_full_subgroups && vk_caps.sg_size && lx && lx % vk_caps.sg_size == 0 && uses_subgroup_ops(code, words))
ci.stage.flags |= VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
VkPipeline pipe = VK_NULL_HANDLE;
crash_note("a compute pipeline", cs, NULL, NULL);
if (vkCreateComputePipelines(P.device, P.cache, 1, &ci, NULL, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;

View File

@@ -74,6 +74,7 @@ typedef struct {
uint32_t cepoch;
view_t cview;
uint8_t chas_view, used, has_view;
uint64_t khash; // hash of the descriptor key and view, 0 for CPU-written textures (vk_texture_slot_khash_locked)
view_t v;
tex_t t;
} TexSlot;
@@ -127,6 +128,7 @@ static uint32_t agen[AGEN_N];
// 20 bits), unlike tslots, which moves when it grows.
static uint64_t *sinfo;
static uint32_t reg_serial;
static uint64_t n_same_regs; // vk_texture_register calls that changed nothing (vk_texture_same_regs)
static uint32_t agen_idx(uint64_t addr) { return (uint32_t)(addr * 0x9E3779B97F4A7C15ull >> 48); }
@@ -145,7 +147,12 @@ static uint16_t *slot_bucket;
static void dirty_push(uint32_t x) { dirty[ndirty++ % DIRTY_N] = x; }
static uint64_t n_bumps, n_site[4]; // tex_gen_bump calls, by site: destroy, create, take-back, release // tex_gen_bump calls, take-backs that undid a release's bump (vk_texture_bucket_stats)
static uint32_t bump_count[AGEN_N];
static void tex_gen_bump(uint64_t addr) {
n_bumps++;
bump_count[agen_idx(addr)]++;
if (!++tex_gen) tex_gen = 1;
slots_gen++;
uint32_t b = agen_idx(addr);
@@ -617,6 +624,7 @@ static void rel_unlink(Tex *e) {
static void destroy_tex(Tex *e) {
own_remove(e);
n_site[0]++;
tex_gen_bump(e->key.addr);
nvn_hm_del(&by_key, &e->key);
Tex *head = nvn_hm_get(&by_addr, &e->key.addr);
@@ -1062,9 +1070,10 @@ static Tex *entry_of(const tex_t *t) {
e->next_at = nvn_hm_get(&by_addr, &k.addr);
nvn_hm_put(&by_addr, &k.addr, e);
nvn_hm_put(&by_key, &k, e);
n_site[1]++;
tex_gen_bump(k.addr);
}
if (e->released) e->released = 0, nreleased--, rel_unlink(e), tex_gen_bump(k.addr);
if (e->released) e->released = 0, nreleased--, rel_unlink(e), n_site[2]++, tex_gen_bump(k.addr);
e->t = *t;
return e;
}
@@ -1158,6 +1167,7 @@ void vk_texture_release(const tex_t *t) {
e->rel_serial = reg_serial;
nreleased++;
rel_append(e);
n_site[3]++;
tex_gen_bump(k.addr);
sweep_released();
}
@@ -1199,6 +1209,16 @@ void vk_texture_register(int id, const tex_t *t, const view_t *v) {
__atomic_store_n(&sinfo, p, __ATOMIC_RELEASE);
}
TexSlot *s = &tslots[id];
// The same texture and view registered again (Ocarina rewrites much of its descriptor pool every frame): the slot
// keeps its serial and resolved entry, so the texture pool refresh (vk_exec_draw.c pool_sync_entries) does not
// resolve it again. Only while that entry is live: after a release, a registration is what makes a slot current
// again (slot_resolve's stale rule), so it goes the full way.
if (s->used && s->ce && !s->ce->released && !memcmp(&s->t, t, sizeof *t) && s->has_view == (v != NULL) &&
(!v || !memcmp(&s->v, v, sizeof *v))) {
n_same_regs++;
unlock();
return;
}
s->t = *t;
s->used = 1;
s->ce = NULL;
@@ -1210,9 +1230,26 @@ void vk_texture_register(int id, const tex_t *t, const view_t *v) {
s->has_view = v != NULL;
if (v) s->v = *v;
else memset(&s->v, 0, sizeof s->v);
s->khash = 0;
if (!tex_mirrored(t)) { // FNV-1a over the key (make_key zeroes its padding) and the view
TexKey k;
make_key(t, &k);
uint64_t h = 0xcbf29ce484222325ull;
const uint8_t *b = (const uint8_t *)&k;
for (size_t i = 0; i < sizeof k; i++) h = (h ^ b[i]) * 0x100000001b3ull;
b = (const uint8_t *)&s->v;
for (size_t i = 0; i < sizeof s->v; i++) h = (h ^ b[i]) * 0x100000001b3ull;
s->khash = (h ^ s->has_view) | 1;
}
unlock();
}
// The descriptor hash of a GPU-written slot: two slots with equal hashes, the same address generation and no entry
// created, destroyed, released or taken back between their lookups (vk_texture_entry_gen) resolve alike. 0 for an
// unregistered or CPU-written slot (its resolution depends on when it was registered: slot_resolve's stale rule).
uint64_t vk_texture_slot_khash_locked(int id) { return id >= 0 && id < ntslots && tslots[id].used ? tslots[id].khash : 0; }
uint32_t vk_texture_entry_gen(void) { return tex_gen; }
// The entry slot s resolves to, and the view and has_view a caller gets with it (the slot's, with the descriptor's
// swizzle when the entry is another descriptor at the same storage). NULL when the slot names no texture.
// Entry creation budget of slot lookups (vk_texture_create_budget_locked): -1 none. The bindless pools look up every
@@ -1289,6 +1326,35 @@ static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view) {
void vk_texture_lock(void) { lock(); }
void vk_texture_unlock(void) { unlock(); }
uint32_t vk_texture_slots_gen(void) { return slots_gen; }
uint64_t vk_texture_same_regs(void) { return __atomic_load_n(&n_same_regs, __ATOMIC_RELAXED); }
// Diagnosis for the texture pool log: address-generation bumps since the last call, the slots of the fullest buckets
// and the bumps those buckets took. Texture lock held by none: takes it.
void vk_texture_bucket_stats(char *out, size_t n) {
lock();
static uint32_t occ[AGEN_N];
memset(occ, 0, sizeof occ);
for (int i = 0; i < ntslots && sinfo; i++)
if (tslots[i].used) occ[sinfo[i] & 0xffff]++;
int top[4] = {-1, -1, -1, -1};
for (int b = 0; b < AGEN_N; b++)
for (int k = 0; k < 4; k++)
if (top[k] < 0 || occ[b] > occ[top[k]]) {
for (int j = 3; j > k; j--) top[j] = top[j - 1];
top[k] = b;
break;
}
uint64_t hot = 0; // bumps weighted by the slots they invalidate
for (int b = 0; b < AGEN_N; b++) hot += (uint64_t)bump_count[b] * occ[b];
snprintf(out, n, "%llu bumps (destroy %llu, create %llu, take-back %llu, release %llu), %llu slot invalidations; fullest buckets %u/%u/%u/%u slots, bumped %u/%u/%u/%u",
(unsigned long long)n_bumps, (unsigned long long)n_site[0], (unsigned long long)n_site[1],
(unsigned long long)n_site[2], (unsigned long long)n_site[3], (unsigned long long)hot, occ[top[0]], occ[top[1]], occ[top[2]], occ[top[3]],
bump_count[top[0]], bump_count[top[1]], bump_count[top[2]], bump_count[top[3]]);
n_bumps = 0;
memset(n_site, 0, sizeof n_site);
memset(bump_count, 0, sizeof bump_count);
unlock();
}
uint64_t vk_texture_slot_token(int id) {
const uint64_t *si = __atomic_load_n(&sinfo, __ATOMIC_ACQUIRE);