mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-12 02:11:03 +00:00
replay_workers = N (0, the default, keeps the sequential replay) records a submission of enough draws in chunks that start between passes, on N threads and in twice as many chunks, and submits them in order. In the town at 4K on an RTX 5070, 4 threads take the replay from 8.2 to 4.5 ms a frame and the frame rate from 71 to 79.
A sequential scan on the submitting thread applies only what is queue state (fixed-function and bind ops, render targets, render enable and conditions, samples-passed counting) and keeps a copy of it where each chunk starts, with how far its lists got: inline uniform updates, RENDER_COND results, contact shadow marches and the ops that drop uniform copies. A chunk takes those from the lists instead of from the chunks before it, starts with every image in GENERAL and ends with a full barrier, so its recording depends only on the copy and the lists.
Each thread records with a context of its own: the replay's memos, descriptor state, occlusion query segments and per-command-buffer state moved from file statics into VkExec and VkExecDraw, and the tables the contexts share (stand-ins, depth twins, views, mirrors, timestamps, completion callbacks, crumbs) take locks or atomics. The scan makes render targets live in replay order and notes the op (vk_texture_revive), since a chunk can sample a storage before the chunk that renders it got there or after the scan went past a later target there: a chunk's texture slots resolve as of its own op, and the chunks after a texture with no entry yet wait for the chunk that makes it. Uniform copies are dropped at the op that writes them (vk_usnap_drop_at).
Contact shadows plan their march in replay order (vk_contact_plan) and the chunk that holds its op records it; the HDR source keeps the copy that comes last in replay order. DLSS and fxaa = 0 act on the frame draw by draw and keep the replay sequential. TOTK_REPLAY_STATE_CHECK=1 compares the state after the last chunk with the scan's.
A draw or dispatch whose shaders write storage buffers drops the uniform copies they cover from its own op on, as the other GPU writes do (scan_writes). The scan still translates no shader: it reads the shaders the recording contexts translated from a table shared with them (vk_exec_draw_writes), and an op whose shader no context translated yet holds the chunks after it until it is recorded. Before, the chunk that recorded such a write dropped the copies for every position at that moment, so a later chunk recorded first bound the copy taken at the submit call: on an RX 7800 XT on RADV the grass near the camera came out dark or unshaded in some runs with 4 threads.
TOTK_TRACE=perf counts the chunks held per submission by reason and logs when each chunk of the last submission began. The settings menu has the thread count under Advanced, Scheduling, with the count this run uses under it.
The recorder thread begins, ends and submits the submission's open part (vk_rec.c rec_end_cb), so a chunk's mirror prologue stays off it: vk_sync_part waits until the recorder is idle, as the part pool allows one thread at a time, then records, ends and submits the prologue itself. Through the recorder the prologue could reach the queue before it was ended, and the GPU lost the device at the title screen.
(cherry picked from commit b94e2e9642)
266 lines
13 KiB
C
266 lines
13 KiB
C
// GPU breadcrumbs for a lost device (vk.h vk_crumb). VK_AMD_buffer_marker writes a 32-bit code into a buffer from
|
|
// the command stream: at TOP_OF_PIPE once the command processor reaches the marker, at BOTTOM_OF_PIPE once every
|
|
// command before it has finished. Each crumb writes both, into two words of one host-coherent buffer, so after a
|
|
// device loss word 0 names the newest point the GPU reached and word 1 the newest point it had finished: the hang lies
|
|
// between them. A code is the crumb kind in the top 4 bits and its number in the low 28. The markers cost two small
|
|
// writes per crumb, and crumbs mark submissions, upload batches, ASTC chunks and presents, not draws.
|
|
//
|
|
// Pass marks (vk.h vk_pass_mark_begin) use words 2 and 3 of the same buffer: the replay marks the start and the end of
|
|
// every render pass and dispatch, and a ring of records says what each mark was. The design follows Savage Wind
|
|
// Recomp's GPU checkpoints (refs/SavageWindRecomp runtime/nvn/vulkan/exec.c ck_mark and vk_checkpoints_report: a mark
|
|
// per pass and dispatch, numbers in a ring of 4096, text only at the device loss); the code is ours.
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "cfg.h"
|
|
#include "host.h"
|
|
#include "vk.h"
|
|
|
|
static PFN_vkCmdWriteBufferMarkerAMD write_marker;
|
|
static VkBuffer buf;
|
|
static VmaAllocation alloc;
|
|
static volatile uint32_t *words; // 0, 1: crumbs; 2, 3: pass marks
|
|
|
|
// Pass marks (below)
|
|
int vk_pass_marks_on;
|
|
static int pm_amd; // marks into words 2 and 3
|
|
static PFN_vkCmdSetCheckpointNV set_checkpoint;
|
|
static void pass_marks_init(VkCtx *c);
|
|
|
|
static void buffer_init(VkCtx *c) {
|
|
write_marker = (PFN_vkCmdWriteBufferMarkerAMD)vkGetDeviceProcAddr(c->device, "vkCmdWriteBufferMarkerAMD");
|
|
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = 16,
|
|
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
|
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
|
|
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
|
|
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
|
|
VmaAllocationInfo info;
|
|
if (!write_marker || vmaCreateBuffer(c->vma, &bi, &ai, &buf, &alloc, &info) != VK_SUCCESS) {
|
|
rt_log("vk: GPU breadcrumbs unavailable");
|
|
write_marker = NULL;
|
|
return;
|
|
}
|
|
words = info.pMappedData;
|
|
words[0] = words[1] = words[2] = words[3] = 0;
|
|
rt_log("vk: GPU breadcrumbs on (VK_AMD_buffer_marker)");
|
|
}
|
|
|
|
void vk_crumbs_init(VkCtx *c) {
|
|
if (c->has_buffer_marker) buffer_init(c);
|
|
pass_marks_init(c);
|
|
}
|
|
|
|
void vk_crumbs_release(void) {
|
|
VkCtx *c = vk_ctx();
|
|
if (buf && c) vmaDestroyBuffer(c->vma, buf, alloc);
|
|
buf = VK_NULL_HANDLE, alloc = NULL, words = NULL, write_marker = NULL;
|
|
vk_pass_marks_on = pm_amd = 0, set_checkpoint = NULL;
|
|
}
|
|
|
|
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n) {
|
|
if (!write_marker || !cb) return;
|
|
uint32_t code = (uint32_t)kind << 28 | (uint32_t)(n & 0x0fffffff);
|
|
write_marker(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, buf, 0, code);
|
|
write_marker(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, buf, 4, code);
|
|
}
|
|
|
|
static void describe(uint32_t code, char *o, size_t size) {
|
|
static const char *const kinds[] = {"nothing", "the start of a part of submission", "the end of submission",
|
|
"the start of upload batch", "the end of upload batch",
|
|
"ASTC decode chunk", "the start of present",
|
|
"the end of present"};
|
|
unsigned k = code >> 28;
|
|
if (!code) snprintf(o, size, "nothing yet");
|
|
else if (k < sizeof kinds / sizeof *kinds) snprintf(o, size, "%s %u", kinds[k], code & 0x0fffffff);
|
|
else snprintf(o, size, "unknown code %#x", code);
|
|
}
|
|
|
|
void vk_crumbs_report(void) {
|
|
if (!words) return;
|
|
char reached[96], finished[96];
|
|
describe(words[0], reached, sizeof reached);
|
|
describe(words[1], finished, sizeof finished);
|
|
rt_log("vk: GPU breadcrumbs: the GPU last reached %s; everything before %s had finished", reached, finished);
|
|
}
|
|
|
|
// ---- pass marks
|
|
|
|
// Cost per pass or dispatch: about twenty stores into the ring and two commands, one at the start and one at the end,
|
|
// each a marker write or a checkpoint. Per draw nothing, except that the branch of vk_exec_draw.c prepare_draw that
|
|
// records changed pipeline state also notes the shaders of a pass's first draw. A full barrier already precedes every
|
|
// pass and dispatch (vk_exec.c vk_exec_pass_begin, vk_exec_draw.c op_dispatch), so the end-of-pipe write adds no wait.
|
|
static VkPassMark ring[VK_PASS_MARKS];
|
|
static uint32_t seq; // marks made (atomic: the recording threads of a chunked replay)
|
|
|
|
// TOTK_PASS_CRUMBS: unset, buffer marks where VK_AMD_buffer_marker exists; 1, also NVIDIA checkpoints; 0, neither.
|
|
int vk_pass_marks_want_nv(void) {
|
|
const char *e = cfg_get("TOTK_PASS_CRUMBS");
|
|
return e && *e && strcmp(e, "0") != 0;
|
|
}
|
|
|
|
static void pass_marks_init(VkCtx *c) {
|
|
const char *e = cfg_get("TOTK_PASS_CRUMBS");
|
|
int off = e && !strcmp(e, "0");
|
|
set_checkpoint = c->has_nv_checkpoints
|
|
? (PFN_vkCmdSetCheckpointNV)vkGetDeviceProcAddr(c->device, "vkCmdSetCheckpointNV") : NULL;
|
|
pm_amd = !off && write_marker && words;
|
|
vk_pass_marks_on = pm_amd || set_checkpoint;
|
|
if (vk_pass_marks_on)
|
|
rt_log("vk: pass marks on (%s%s%s; TOTK_PASS_CRUMBS=0 turns them off%s)", pm_amd ? "VK_AMD_buffer_marker" : "",
|
|
pm_amd && set_checkpoint ? " and " : "", set_checkpoint ? "VK_NV_device_diagnostic_checkpoints" : "",
|
|
set_checkpoint ? "" : ", =1 adds NVIDIA checkpoints where the driver has them");
|
|
else
|
|
rt_log("vk: pass marks off (%s)", off ? "TOTK_PASS_CRUMBS=0"
|
|
: "no VK_AMD_buffer_marker; TOTK_PASS_CRUMBS=1 asks for NVIDIA checkpoints");
|
|
}
|
|
|
|
void vk_pass_mark_cmd(VkCommandBuffer cb, uint32_t id, int end) {
|
|
if (pm_amd)
|
|
write_marker(cb, end ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, buf,
|
|
end ? 12 : 8, id);
|
|
if (set_checkpoint) set_checkpoint(cb, (const void *)(uintptr_t)((uint64_t)id << 1 | (end != 0)));
|
|
}
|
|
|
|
VkPassMark *vk_pass_mark_begin(VkCommandBuffer cb, int kind, uint64_t fence) {
|
|
if (!vk_pass_marks_on || !cb) return NULL;
|
|
uint32_t id = __atomic_add_fetch(&seq, 1, __ATOMIC_RELAXED); // the recording threads of a chunked replay
|
|
if (!id) id = __atomic_add_fetch(&seq, 1, __ATOMIC_RELAXED); // 0 means no mark
|
|
VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
|
|
m->id = id, m->kind = (uint8_t)kind, m->fence = fence, m->draws = UINT32_MAX;
|
|
rec_pass_mark(cb, id, 0);
|
|
return m;
|
|
}
|
|
|
|
void vk_pass_mark_end(VkCommandBuffer cb, const VkPassMark *m) {
|
|
if (m && cb) rec_pass_mark(cb, m->id, 1);
|
|
}
|
|
|
|
int vk_pass_marks_read(uint32_t *started, uint32_t *finished) {
|
|
if (!pm_amd) return 0;
|
|
*started = words[2], *finished = words[3];
|
|
return 1;
|
|
}
|
|
|
|
const VkPassMark *vk_pass_mark_record(uint32_t id) {
|
|
const VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
|
|
return id && m->id == id ? m : NULL;
|
|
}
|
|
|
|
// The first 8 bytes of the shader's input id (gfx_shader.c), the start of its stored input's name <cache>/in/<id>.in.
|
|
static unsigned long long shader_id(const GfxShader *s) {
|
|
if (!s) return 0;
|
|
const uint8_t *iid = gfx_shader_input_id(s);
|
|
unsigned long long v = 0;
|
|
for (int i = 0; i < 8; i++) v = v << 8 | iid[i];
|
|
return v;
|
|
}
|
|
|
|
static void describe_mark(uint32_t id, char *o, size_t size) {
|
|
const VkPassMark *m = vk_pass_mark_record(id);
|
|
if (!id) {
|
|
snprintf(o, size, "nothing yet");
|
|
return;
|
|
}
|
|
if (!m) {
|
|
snprintf(o, size, "no record (a later mark took its slot)");
|
|
return;
|
|
}
|
|
if (m->kind == VK_MARK_DISPATCH) {
|
|
snprintf(o, size, "dispatch %u of submission %llu: %ux%ux%u groups%s, cs %016llx", m->step,
|
|
(unsigned long long)m->fence, m->w, m->h, m->d, m->w ? "" : " (indirect)", shader_id(m->first[0]));
|
|
return;
|
|
}
|
|
size_t n = (size_t)snprintf(o, size, "pass %u of submission %llu: %ux%u, %u layer%s, colour formats", m->step,
|
|
(unsigned long long)m->fence, m->w, m->h, m->d, m->d == 1 ? "" : "s");
|
|
for (int i = 0; i < m->ncolor && i < 8 && n < size; i++)
|
|
n += (size_t)snprintf(o + n, size - n, "%s%d", i ? "," : " ", (int)m->color[i]);
|
|
if (!m->ncolor && n < size) n += (size_t)snprintf(o + n, size - n, " none");
|
|
if (n < size) n += (size_t)snprintf(o + n, size - n, ", depth format %d", (int)m->depth);
|
|
if (n >= size) return;
|
|
if (m->draws == UINT32_MAX)
|
|
snprintf(o + n, size - n, ", still recording; first draw vs %016llx fs %016llx", shader_id(m->first[0]),
|
|
shader_id(m->first[1]));
|
|
else if (!m->draws)
|
|
snprintf(o + n, size - n, ", no draws");
|
|
else
|
|
snprintf(o + n, size - n, ", %u draws; first vs %016llx fs %016llx, last vs %016llx fs %016llx", m->draws,
|
|
shader_id(m->first[0]), shader_id(m->first[1]), shader_id(m->last[0]), shader_id(m->last[1]));
|
|
}
|
|
|
|
void vk_pass_marks_format(const char *source, uint32_t started, uint32_t finished, char *o, size_t size) {
|
|
char a[640], b[640];
|
|
describe_mark(started, a, sizeof a);
|
|
describe_mark(finished, b, sizeof b);
|
|
snprintf(o, size,
|
|
"vk: pass marks (%s, %u made): the GPU last started mark %u and last finished mark %u\n"
|
|
"vk: last started %u: %s\n"
|
|
"vk: last finished %u: %s",
|
|
source, (unsigned)seq, (unsigned)started, (unsigned)finished, (unsigned)started, a, (unsigned)finished, b);
|
|
}
|
|
|
|
static void log_lines(char *t) {
|
|
for (char *line = t, *nl; line && *line; line = nl ? nl + 1 : NULL) {
|
|
nl = strchr(line, '\n');
|
|
if (nl) *nl = 0;
|
|
rt_log("%s", line);
|
|
}
|
|
}
|
|
|
|
// vkGetQueueCheckpointData2NV, else vkGetQueueCheckpointDataNV, on the graphics queue (the replay's): one entry per
|
|
// stage with the last checkpoint that stage executed. A checkpoint is the mark's id times 2, plus 1 at its end, so the
|
|
// top-of-pipe entry names the last start or end the GPU reached, and the bottom-of-pipe entry the last one before which
|
|
// everything had finished: at a start, that is the end of the mark before it.
|
|
static void nv_report(void) {
|
|
VkCtx *c = vk_ctx();
|
|
if (!c || !set_checkpoint) return;
|
|
PFN_vkGetQueueCheckpointData2NV get2 =
|
|
(PFN_vkGetQueueCheckpointData2NV)vkGetDeviceProcAddr(c->device, "vkGetQueueCheckpointData2NV");
|
|
PFN_vkGetQueueCheckpointDataNV get1 =
|
|
(PFN_vkGetQueueCheckpointDataNV)vkGetDeviceProcAddr(c->device, "vkGetQueueCheckpointDataNV");
|
|
enum { MAXN = 32 };
|
|
uint64_t stage[MAXN], marker[MAXN];
|
|
uint32_t n = 0;
|
|
if (get2) {
|
|
VkCheckpointData2NV d[MAXN];
|
|
get2(c->gfx_queue, &n, NULL);
|
|
if (n > MAXN) n = MAXN;
|
|
for (uint32_t i = 0; i < n; i++) d[i] = (VkCheckpointData2NV){.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_2_NV};
|
|
get2(c->gfx_queue, &n, d);
|
|
for (uint32_t i = 0; i < n; i++) stage[i] = d[i].stage, marker[i] = (uint64_t)(uintptr_t)d[i].pCheckpointMarker;
|
|
} else if (get1) {
|
|
VkCheckpointDataNV d[MAXN];
|
|
get1(c->gfx_queue, &n, NULL);
|
|
if (n > MAXN) n = MAXN;
|
|
for (uint32_t i = 0; i < n; i++) d[i] = (VkCheckpointDataNV){.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV};
|
|
get1(c->gfx_queue, &n, d);
|
|
for (uint32_t i = 0; i < n; i++) stage[i] = d[i].stage, marker[i] = (uint64_t)(uintptr_t)d[i].pCheckpointMarker;
|
|
} else {
|
|
rt_log("vk: pass marks: no vkGetQueueCheckpointDataNV entry point");
|
|
return;
|
|
}
|
|
uint32_t started = 0, finished = 0;
|
|
for (uint32_t i = 0; i < n; i++) {
|
|
uint32_t id = (uint32_t)(marker[i] >> 1);
|
|
int end = (int)(marker[i] & 1);
|
|
rt_log("vk: checkpoint of stage %#llx: the %s of mark %u", (unsigned long long)stage[i], end ? "end" : "start", id);
|
|
if (stage[i] == VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT) started = id;
|
|
if (stage[i] == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT) finished = end ? id : id ? id - 1 : 0;
|
|
}
|
|
if (!n) rt_log("vk: pass marks: the driver reported no checkpoints");
|
|
char t[2048];
|
|
vk_pass_marks_format("VK_NV_device_diagnostic_checkpoints", started, finished, t, sizeof t);
|
|
log_lines(t);
|
|
}
|
|
|
|
void vk_pass_marks_report(void) {
|
|
if (!vk_pass_marks_on) return;
|
|
uint32_t started, finished;
|
|
char t[2048];
|
|
if (vk_pass_marks_read(&started, &finished)) {
|
|
vk_pass_marks_format("VK_AMD_buffer_marker", started, finished, t, sizeof t);
|
|
log_lines(t);
|
|
}
|
|
nv_report();
|
|
}
|