mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
A draw that misses its pipeline has the rest of its submission read ahead, and the stage libraries of the pipelines the later draws need are queued at once.
The next draw with a new pipeline was only found when the replay reached it, so the draws of an area's first frame, or of a turn that brings several new materials into view, paid for their pipelines one after another while the workers that could have built them together sat idle. The submission is recorded before it is replayed: the first draw of a submission that finds a shader or pipeline missing now has the ops after it read (vk_exec.c vk_exec_ahead) with the binds that decide a draw's shaders and shader libraries applied to a copy of the replay state. Each draw there looks its shaders up as the real draw will, which also queues a shader that is not in memory, and names its state to vk_pipeline_hint, which queues the pre-rasterization and fragment shader libraries that are missing. No pipeline is entered and no key recorded, so a wrong guess costs two libraries at most. The draw's own lookup then links on its thread when both libraries are there. On a first visit to Kokiri Forest a scan read 6,900 ops in 0.78 ms on average (the longest 5.5 ms; a scan ends after 3 ms), and the scans of the load and the walk named 377 states without a pipeline and queued 104 libraries for them. pipe_ahead = 0 (NX2NAVI_PIPE_AHEAD) turns it off. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -121,7 +121,14 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
|
||||
ring of Vulkan `{x, y, z}` records filled on the GPU.
|
||||
- Pipelines: `VK_EXT_graphics_pipeline_library` where the driver has it: stage libraries compiled ahead, a fast link
|
||||
at a new state, an optimised pipeline replacing it from a background thread. A draw whose pipeline is not ready is
|
||||
skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise. Compute pipelines
|
||||
skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise. Three things keep
|
||||
that short on a first visit (`runtime/vk_pipeline.c`): a pipeline whose fragment shader reads a target of its own
|
||||
pass is fast-linked like any other, the feedback loop flags being part of the fragment output library alone
|
||||
(`gpl_feedback`); the library workers also link the pipelines of skipped draws, so a burst of new materials is
|
||||
linked on several threads while the pool is inside optimised builds (`lib_worker`, `fast_take`); and the first
|
||||
draw of a submission that misses has the rest of the submission read ahead, which queues the stage libraries of
|
||||
every pipeline the later draws need before the replay reaches them (`runtime/vk_exec.c` `vk_exec_ahead`,
|
||||
`vk_pipeline_hint`). Mesh pipelines have no libraries and are built whole. Compute pipelines
|
||||
are compiled outside the pipeline lock, and one compile serves every thread that needs it. The boot builds the
|
||||
pipeline of every compute shader play recorded or the shader preparation listed, but keeps only those of the shaders
|
||||
dispatched most recently (`compute-used.bin`, `compute_keep`): the others go into the pipeline cache and are
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
# How many milliseconds the draws of one frame may wait, in all, for a missing pipeline to be linked from the shader
|
||||
# libraries in the graphics driver's cache before they are dropped (0 to 100; 0: dropped at once, as before).
|
||||
# pipe_link_ms = 6
|
||||
# When a draw finds its pipeline missing, the rest of the commands the game submitted with it are read ahead and the
|
||||
# shader libraries of every other pipeline they will need are built at once on background threads, so the later draws
|
||||
# find theirs ready instead of waiting one after another or being left out of a frame. 0: off, as before.
|
||||
# pipe_ahead = 1
|
||||
# A pipeline whose fragment shader reads the picture it draws into (soft particles, water, some full-screen passes) is
|
||||
# linked from the shader libraries like any other. 0: as before, such a draw waits for the whole compile.
|
||||
# gpl_feedback = 1
|
||||
|
||||
@@ -43,7 +43,7 @@ static const char *const known[] = {
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLSS_IDLE_FREE", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREE_CORES",
|
||||
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "PIPE_FAST_WORKERS", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_BLOWOUT", "HDR_CONTRAST",
|
||||
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_BLOWOUT", "HDR_CONTRAST",
|
||||
"HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GAME_NITS", "HDR_GRADE", "HDR_HIGHLIGHT_SAT", "HDR_HIGHLIGHTS", "HDR_HUE",
|
||||
"HDR_LIVE",
|
||||
"HDR_PEAK_NITS", "HDR_SATURATION", "HDR_SHADOWS", "HDR_TONEMAP", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
@@ -114,7 +114,7 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"POOL_CREATE_MS", "performance"}, {"POOL_LOAD_BACKLOG", "performance"}, {"POOL_LOAD_MS", "performance"},
|
||||
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"}, {"ONESHOT_BUDGET", "performance"},
|
||||
{"CACHE_DIE", "performance"}, {"ONE_PER_CORE", "performance"},
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"LIB_WARM", "performance"}, {"PIPE_LINK_MS", "performance"}, {"PIPE_FAST_WORKERS", "performance"}, {"GPL_FEEDBACK", "performance"}, {"FRAME_HOLD_MS", "performance"}, {"PACE_SMOOTH", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"LIB_WARM", "performance"}, {"PIPE_LINK_MS", "performance"}, {"PIPE_AHEAD", "performance"}, {"PIPE_FAST_WORKERS", "performance"}, {"GPL_FEEDBACK", "performance"}, {"FRAME_HOLD_MS", "performance"}, {"PACE_SMOOTH", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
{"COMPUTE_KEEP", "performance"},
|
||||
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_DISUSE", "performance"}, {"TEX_GEN_UNDO", "performance"}, {"TEX_REUSE", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"TEX_PROC_COMMIT_MB", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
@@ -218,6 +218,7 @@ static const CfgMeta metas[] = {
|
||||
// shaders and pipelines
|
||||
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "any", "strict|any|fullscreen", RESTART},
|
||||
{"PIPE_LINK_MS", "6", "0..100", RESTART}, // vk_exec_draw.c link_budget
|
||||
{"PIPE_AHEAD", "1", "0|1", RESTART}, // vk_exec.c vk_exec_ahead, vk_pipeline.c vk_pipeline_hint
|
||||
{"GPL_FEEDBACK", "1", "0|1", RESTART}, // vk_pipeline.c gpl_feedback
|
||||
{"PIPE_FAST_WORKERS", "1", "0|1", RESTART}, // vk_pipeline.c lib_worker, fast_take
|
||||
{"FRAME_HOLD_MS", "120", "0..500", RESTART}, // vk_backend.c frame_hold
|
||||
|
||||
@@ -701,6 +701,9 @@ VkPipeline vk_pipeline_graphics(const VkPipeState *s, const VkPipeInfo **info);
|
||||
// wait_ms > 0 waits up to that long for a pending pipeline first. Pipelines built in a run are recorded to
|
||||
// pipeline-keys.bin next to the pipeline cache and prebuilt at the next boot (NX2NAVI_PIPE_PREBUILD=0: not).
|
||||
VkPipeline vk_pipeline_graphics_async(const VkPipeState *s, const VkPipeInfo **info, int *pending, uint32_t wait_ms);
|
||||
// The stage libraries of a draw the replay has not reached yet, queued to the workers when no pipeline is known for its
|
||||
// state (vk_pipeline.c "stage libraries ahead of their draws"). Returns the libraries queued.
|
||||
int vk_pipeline_hint(const VkPipeState *s);
|
||||
// rt_boot calls it unless --shot is given; NX2NAVI_PIPE_SYNC=1 makes it a no-op. Of the probes only --probe-prewarm
|
||||
// calls it.
|
||||
void vk_pipeline_async_enable(void);
|
||||
|
||||
@@ -1136,11 +1136,125 @@ static void evo_resume(VkExec *ex) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- looking ahead for pipelines (NX2NAVI_PIPE_AHEAD, default 1; 0 off)
|
||||
// A draw whose pipeline is not there yet waits for its fast link or is skipped until a worker has built it, and the
|
||||
// next draw with a new pipeline is only found when the replay reaches it: an area's first frame, or a turn that brings
|
||||
// a dozen new materials into view, paid for them one after another (each wait a few milliseconds with the stage
|
||||
// libraries in the driver's cache, 100 ms and more without), or showed frames with those draws missing while the
|
||||
// workers that could have built them all at once sat idle.
|
||||
// The submission is recorded before it is replayed, so what comes after the draw is known. The first draw of a
|
||||
// submission that finds a shader or pipeline missing has the rest read here (vk_exec_ahead), from the op after it to
|
||||
// the end of the submission's last command buffer: the scan applies the binds that decide a draw's shaders and its
|
||||
// shader libraries (program, state objects, vertex attributes, textures and images, render targets, depth clamp) to a
|
||||
// copy of the replay state and, at each draw, looks its shaders up as the draw will and names the state to
|
||||
// vk_pipeline_hint, which queues the libraries not built yet. Nothing is recorded and no Vulkan object is touched;
|
||||
// a shader not in memory is queued for its disk read or translation by the lookup itself. The draws then find their
|
||||
// libraries built, or building on several workers at once.
|
||||
// What the scan does not follow: the op stream past a wait for another queue's signal (what follows may be held), a
|
||||
// change of the texture pool, mesh draws (their pipelines have no libraries), and whatever would take it past
|
||||
// AHEAD_NS. Render targets are read from their descriptors (format, view format, sample count), which is what the
|
||||
// replay resolves them to unless a target is left out; a guess that misses costs two libraries at most.
|
||||
#define AHEAD_NS 3000000ull
|
||||
#define AHEAD_OPS 400000u
|
||||
static struct {
|
||||
int on, live; // on: -1 until read; live: vk_exec_record walks handles[i] now
|
||||
const uint64_t *handles;
|
||||
int n, i;
|
||||
const op_hdr *at[CALL_DEPTH + 1]; // the op being replayed at each call depth
|
||||
int depth;
|
||||
uint64_t seq; // the submission scanned last
|
||||
uint64_t scans, ops, cut, ns, max_ns, logged_scans;
|
||||
} AH = {.on = -1};
|
||||
|
||||
static void ahead_set_rt(VkExec *sx, const p_set_rt *r) {
|
||||
sx->rt = *r;
|
||||
int n = r->n < 0 ? 0 : r->n > 8 ? 8 : r->n, samples = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
const tex_t *t = i < n ? r->color[i] : NULL;
|
||||
sx->pipe.color_fmt[i] = !t ? 0 : r->has_cview[i] && r->cview[i].set_format ? (int32_t)r->cview[i].format : t->d.format;
|
||||
if (t && !samples) samples = t->d.samples > 1 ? t->d.samples : 1;
|
||||
}
|
||||
sx->pipe.depth_fmt = r->depth ? r->depth->d.format : 0;
|
||||
if (r->depth && !samples) samples = r->depth->d.samples > 1 ? r->depth->d.samples : 1;
|
||||
sx->pipe.samples = (uint8_t)(samples ? samples : 1);
|
||||
}
|
||||
|
||||
void vk_exec_ahead(VkExec *ex) {
|
||||
if (AH.on < 0) {
|
||||
AH.on = cfg_bool("NX2NAVI_PIPE_AHEAD");
|
||||
rt_log("vk: a draw that finds its pipeline missing has the rest of its submission read ahead for the stage "
|
||||
"libraries the later draws need: %s (NX2NAVI_PIPE_AHEAD)", AH.on ? "on" : "off");
|
||||
}
|
||||
if (!AH.on || !AH.live || evo_in || AH.seq == vk_exec_submit_seq) return;
|
||||
AH.seq = vk_exec_submit_seq;
|
||||
uint64_t t0 = kern_now_ns();
|
||||
uint32_t ops = 0;
|
||||
static VkExec sx; // a copy of the replay state: too large for the stack
|
||||
sx = *ex;
|
||||
vk_exec_ahead_begin();
|
||||
const op_hdr *ret[CALL_DEPTH + 1];
|
||||
int depth = AH.depth, i = AH.i;
|
||||
for (int d = 0; d < depth; d++) ret[d] = AH.at[d];
|
||||
const op_hdr *h = nvn_op_next(AH.at[depth]);
|
||||
for (;;) {
|
||||
if (h->size < sizeof *h || (h->size & 7) || h->op >= OP_COUNT_) break; // the replay reports it when it gets there
|
||||
if (++ops >= AHEAD_OPS || (!(ops & 63) && kern_now_ns() - t0 > AHEAD_NS)) {
|
||||
AH.cut++;
|
||||
break;
|
||||
}
|
||||
const void *a = nvn_op_payload(h);
|
||||
int stop = 0;
|
||||
switch (h->op) {
|
||||
case OP_END:
|
||||
if (depth > 0) h = ret[--depth]; // back in the caller: on from the op after its call, below
|
||||
else if (++i < AH.n) {
|
||||
h = nvn_op_first(AH.handles[i]);
|
||||
continue;
|
||||
} else stop = 1;
|
||||
break;
|
||||
case OP_JUMP: h = (const op_hdr *)(uintptr_t)((const p_jump *)a)->target; continue;
|
||||
case OP_CALL:
|
||||
if (depth >= CALL_DEPTH) {
|
||||
stop = 1;
|
||||
break;
|
||||
}
|
||||
ret[depth++] = h;
|
||||
h = nvn_op_first(((const p_jump *)a)->target);
|
||||
continue;
|
||||
case OP_WAIT_EVENT: stop = 1; break;
|
||||
case OP_SET_TEX_POOL: stop = ((const p_set_pool *)a)->pool != sx.tex_pool; break;
|
||||
case OP_SET_RT:
|
||||
if (memcmp(&sx.rt, a, sizeof sx.rt)) ahead_set_rt(&sx, a);
|
||||
break;
|
||||
case OP_DEPTH_CLAMP: sx.pipe.depth_clamp = ((const p_flag *)a)->value != 0; break;
|
||||
case OP_RASTER_DISCARD: sx.pipe.raster_discard = ((const p_flag *)a)->value != 0; break;
|
||||
case OP_BIND_PROGRAM: case OP_BIND_STATE: case OP_BIND_VATTR: case OP_BIND_VSTREAM: case OP_BIND_TEX: case OP_BIND_IMG:
|
||||
case OP_DRAW: case OP_DRAW_IDX: case OP_DRAW_IDX_IND:
|
||||
vk_exec_ahead_op(&sx, h->op, a);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
if (stop) break;
|
||||
h = nvn_op_next(h);
|
||||
}
|
||||
uint64_t ns = kern_now_ns() - t0;
|
||||
AH.scans++, AH.ops += ops, AH.ns += ns;
|
||||
if (ns > AH.max_ns) AH.max_ns = ns;
|
||||
if ((rt_trace & RT_TRACE_PERF) && AH.scans - AH.logged_scans >= 100) {
|
||||
rt_log("perf: looking ahead: %llu scans so far, %.0f ops and %.2f ms each (longest %.2f ms, %llu ended at their "
|
||||
"limit); %llu draws seen, %llu looked up", (unsigned long long)AH.scans, (double)AH.ops / (double)AH.scans,
|
||||
(double)AH.ns / 1e6 / (double)AH.scans, (double)AH.max_ns / 1e6, (unsigned long long)AH.cut,
|
||||
(unsigned long long)vk_exec_ahead_draws, (unsigned long long)vk_exec_ahead_lookups);
|
||||
AH.logged_scans = AH.scans;
|
||||
}
|
||||
}
|
||||
|
||||
static const op_hdr *walk(VkExec *ex, const op_hdr *h, int depth) {
|
||||
for (;;) {
|
||||
if (h->size < sizeof *h || (h->size & 7) || h->op >= OP_COUNT_)
|
||||
rt_fatal("vk: corrupt command record at %p: op %u size %u", (const void *)h, h->op, h->size);
|
||||
const void *a = nvn_op_payload(h);
|
||||
AH.at[depth] = h, AH.depth = depth; // where vk_exec_ahead reads on from
|
||||
switch (h->op) {
|
||||
case OP_END: return NULL;
|
||||
case OP_BEGIN: break;
|
||||
@@ -1320,13 +1434,16 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t
|
||||
if (n) cont_push(slot, cont_new(handles, n, 0, NULL)), evo_held++;
|
||||
} else {
|
||||
evo_queue = evo ? gfx_submit_queue : 0;
|
||||
AH.handles = handles, AH.n = n, AH.live = 1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
AH.i = i;
|
||||
const op_hdr *stop = walk(ex, nvn_op_first(handles[i]), 0);
|
||||
if (!stop) continue;
|
||||
cont_push(slot, cont_new(handles, n, i, stop)); // the rest of this submission waits for its signal
|
||||
evo_held++;
|
||||
break;
|
||||
}
|
||||
AH.live = 0;
|
||||
evo_queue = 0;
|
||||
}
|
||||
vk_exec_pass_end(ex);
|
||||
|
||||
@@ -162,6 +162,14 @@ void vk_exec_noatt_dims(const VkExec *ex, uint32_t *w, uint32_t *h); // pass s
|
||||
// rendered for the first time) was rendered in the previous presented frame (per-frame targets); 0 otherwise, or when
|
||||
// the pass has none.
|
||||
int vk_exec_targets_recent(VkExec *ex, int any);
|
||||
// Looking ahead (vk_exec.c): called by a draw that found its shader or pipeline missing, before it waits or is skipped.
|
||||
// The rest of the submission being replayed is read once and the stage libraries of the pipelines its later draws need
|
||||
// are queued to the workers (vk_pipeline_hint). vk_exec_ahead_op (vk_exec_draw.c) applies one bind or draw op of that
|
||||
// scan to sx, a copy of the replay state; vk_exec_ahead_begin starts a scan there.
|
||||
void vk_exec_ahead(VkExec *ex);
|
||||
void vk_exec_ahead_begin(void);
|
||||
void vk_exec_ahead_op(VkExec *sx, uint32_t op, const void *payload);
|
||||
extern uint64_t vk_exec_ahead_draws, vk_exec_ahead_lookups; // draws the scans saw, and those whose shaders they looked up
|
||||
// The current pass's targets with their formats, extents and the frames they were last rendered in (diagnostics).
|
||||
void vk_exec_targets_describe(VkExec *ex, char *buf, size_t cap);
|
||||
|
||||
|
||||
@@ -3510,6 +3510,7 @@ static int prepare_draw_ex(VkExec *ex, int prim, int32_t count, int restart, int
|
||||
// session. A draw into per-frame targets is skipped until they are ready and comes back next frame.
|
||||
int oneshot = -1, skip = pipe_skip_mode(); // -1: not looked up
|
||||
if (vs_wait || fs_wait || ts_wait) {
|
||||
vk_exec_ahead(ex); // the later draws' shaders and stage libraries are queued while this one waits or is skipped
|
||||
if (!(oneshot = !vk_exec_targets_recent(ex, skip >= 1))) // skipped until ready
|
||||
return perf_count(PERF_CNT_SHADER_SKIP), draw_skipped(ex, mesh, SK_SHADER_PENDING);
|
||||
perf_count(PERF_CNT_ONESHOT_WAIT);
|
||||
@@ -3572,15 +3573,15 @@ static int prepare_draw_ex(VkExec *ex, int prim, int32_t count, int restart, int
|
||||
info = PM.info, pending = 0;
|
||||
p = __atomic_load_n(&info->pipeline, __ATOMIC_ACQUIRE);
|
||||
} else {
|
||||
if (linking) { // nearly every draw finds its pipeline: the clock is read only for one that did not
|
||||
p = vk_pipeline_graphics_async(&s, &info, &pending, 0);
|
||||
if (pending) {
|
||||
t0 = kern_now_ns();
|
||||
p = vk_pipeline_graphics_async(&s, &info, &pending, wait_ms);
|
||||
}
|
||||
} else {
|
||||
w0 = t0 ? t0 : wait_ms ? kern_now_ns() : 0; // the clock is read only for draws that may wait
|
||||
p = vk_pipeline_graphics_async(&s, &info, &pending, wait_ms);
|
||||
// Nearly every draw finds its pipeline, so the first lookup waits for nothing and the clock is read only for a
|
||||
// draw that missed. A miss first has the rest of the submission read ahead (vk_exec_ahead: the stage libraries
|
||||
// of the later draws' pipelines build on the workers from here on), inside this draw's wait budget.
|
||||
p = vk_pipeline_graphics_async(&s, &info, &pending, 0);
|
||||
if (pending) {
|
||||
if (linking) t0 = kern_now_ns();
|
||||
else w0 = t0 ? t0 : wait_ms ? kern_now_ns() : 0;
|
||||
vk_exec_ahead(ex);
|
||||
if (wait_ms) p = vk_pipeline_graphics_async(&s, &info, &pending, wait_ms);
|
||||
}
|
||||
if (t0 && budget) {
|
||||
uint64_t spent = kern_now_ns() - t0;
|
||||
@@ -3670,6 +3671,89 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
|
||||
return prepare_draw_ex(ex, prim, count, restart, 0);
|
||||
}
|
||||
|
||||
// ---- looking ahead (vk_exec.c vk_exec_ahead): the binds and draws of ops the replay has not reached, applied to sx, a
|
||||
// copy of the replay state. A draw looks its shaders up as prepare_draw_ex will (one not in memory is queued for its
|
||||
// disk read or translation by that lookup) and names its state to vk_pipeline_hint. The last draw looked up is kept:
|
||||
// one with the same program, texture binds and state repeats it, as most draws of a pass do.
|
||||
uint64_t vk_exec_ahead_draws, vk_exec_ahead_lookups;
|
||||
static struct { int valid; const prog_t *prog; uint64_t serial; uint32_t tex_serial; VkPipeState pipe; } AHL;
|
||||
|
||||
void vk_exec_ahead_begin(void) { AHL.valid = 0; }
|
||||
|
||||
static void ahead_draw(VkExec *sx) {
|
||||
vk_exec_ahead_draws++;
|
||||
if (!sx->prog) return;
|
||||
if (AHL.valid && AHL.prog == sx->prog && AHL.serial == sx->prog->serial && AHL.tex_serial == sx->tex_serial &&
|
||||
!memcmp(&AHL.pipe, &sx->pipe, sizeof AHL.pipe))
|
||||
return;
|
||||
AHL.valid = 1, AHL.prog = sx->prog, AHL.serial = sx->prog->serial, AHL.tex_serial = sx->tex_serial, AHL.pipe = sx->pipe;
|
||||
vk_exec_ahead_lookups++;
|
||||
int has_vs, has_fs, vs_wait = 0, fs_wait = 0;
|
||||
const GfxShader *vs = shader_for(sx, NVN_STAGE_VS, &has_vs, &vs_wait, 0);
|
||||
if (!has_vs) return; // a mesh program, or none: no libraries
|
||||
const GfxShader *fs = shader_for(sx, NVN_STAGE_FS, &has_fs, &fs_wait, 0);
|
||||
if (!vs || vs_wait || fs_wait || (has_fs && !fs) || shader_skipped(vs) || shader_skipped(fs)) return;
|
||||
VkPipeState s = sx->pipe;
|
||||
s.vs = vs, s.fs = fs, s.ts = NULL;
|
||||
s.feedback = 0;
|
||||
s.prim = NVN_DRAW_PRIMITIVE_TRIANGLES;
|
||||
s.depth_minus_w = (uint8_t)(nvn_depth_minus_w() != 0);
|
||||
s.y_flip = 1;
|
||||
s.prim_restart = 0;
|
||||
vk_pipeline_hint(&s);
|
||||
}
|
||||
|
||||
void vk_exec_ahead_op(VkExec *sx, uint32_t op, const void *payload) {
|
||||
switch (op) {
|
||||
case OP_BIND_PROGRAM: {
|
||||
const p_bind_program *b = payload;
|
||||
sx->prog = b->prog, sx->prog_stages = b->stages;
|
||||
break;
|
||||
}
|
||||
case OP_BIND_STATE: {
|
||||
const p_bind_state *b = payload;
|
||||
blend_st bs;
|
||||
switch (b->kind) {
|
||||
case 0:
|
||||
memcpy(&bs, b->bytes, sizeof bs);
|
||||
if (bs.target < 8) sx->pipe.blend[bs.target] = bs;
|
||||
break;
|
||||
case 1: memcpy(&sx->pipe.color, b->bytes, sizeof sx->pipe.color); break;
|
||||
case 2: memcpy(&sx->pipe.chmask, b->bytes, sizeof sx->pipe.chmask); break;
|
||||
case 3: memcpy(&sx->pipe.ms, b->bytes, sizeof sx->pipe.ms); break;
|
||||
case 4: memcpy(&sx->pipe.poly, b->bytes, sizeof sx->pipe.poly); break;
|
||||
case 5: memcpy(&sx->pipe.ds, b->bytes, sizeof sx->pipe.ds); break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OP_BIND_VATTR: {
|
||||
const p_bind_vattr *b = payload;
|
||||
if (b->first < 0 || b->n < 0 || b->first + b->n > 16) break;
|
||||
memset(sx->pipe.vattr, 0, sizeof sx->pipe.vattr);
|
||||
memcpy(&sx->pipe.vattr[b->first], b->st, sizeof(uint32_t) * (size_t)b->n);
|
||||
break;
|
||||
}
|
||||
case OP_BIND_VSTREAM: {
|
||||
const p_bind_vstream *b = payload;
|
||||
if (b->first < 0 || b->n < 0 || b->first + b->n > 16) break;
|
||||
memset(sx->pipe.vstream, 0, sizeof sx->pipe.vstream);
|
||||
memcpy(&sx->pipe.vstream[b->first], b->st, sizeof(vs_st) * (size_t)b->n);
|
||||
break;
|
||||
}
|
||||
case OP_BIND_TEX: case OP_BIND_IMG: {
|
||||
const p_bind_handles *b = payload;
|
||||
int cap = op == OP_BIND_TEX ? 128 : 32;
|
||||
if (b->stage < 0 || b->stage >= 8 || b->first < 0 || b->n < 0 || b->n > 32 || b->first + b->n > cap) break;
|
||||
memcpy(op == OP_BIND_TEX ? &sx->tex[b->stage][b->first] : &sx->img[b->stage][b->first], b->h,
|
||||
sizeof(uint64_t) * (size_t)b->n);
|
||||
sx->tex_serial++;
|
||||
break;
|
||||
}
|
||||
case OP_DRAW: case OP_DRAW_IDX: case OP_DRAW_IDX_IND: ahead_draw(sx); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- per-object motion
|
||||
// The camera pass (vk_dlaa.c mv_pass) gives everything the camera's motion, so a character that moves with the camera,
|
||||
// or on its own, gets the wrong one and DLSS throws its history away there. Each solid scene draw whose position reads
|
||||
|
||||
@@ -75,7 +75,7 @@ static struct {
|
||||
int indep; // stage-independent layouts (vk_caps.gpl): vertex set 0, fragment set 1
|
||||
int push_stage; // GFX_STAGE_* whose set is pushed with independent layouts
|
||||
int gpl; // draws whose pipeline is building get a fast-linked one (vk_caps.gpl)
|
||||
struct { uint64_t links, fb_links, wlinks, flinks, dlinks, pr_added, lib_slow[4], ready_n[6], ready_nofast, requeued, link_fail, defer, lib_n[4], lib_fail, swaps, slow, pre_used[4]; double pr_added_ms, lib_slow_ms[4], ready_ms, ready_max, link_ms, link_max, lib_ms[4], lib_max[4], dlink_ms, dlink_max; } g;
|
||||
struct { uint64_t links, fb_links, wlinks, flinks, dlinks, pr_added, lib_slow[4], ready_n[6], ready_nofast, hints, hint_libs, requeued, link_fail, defer, lib_n[4], lib_fail, swaps, slow, pre_used[4]; double pr_added_ms, lib_slow_ms[4], ready_ms, ready_max, 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, f_full_subgroups;
|
||||
@@ -3396,6 +3396,8 @@ static void pipe_worker(void *arg) {
|
||||
return;
|
||||
}
|
||||
|
||||
static int hint_on(void);
|
||||
|
||||
// Starts the pool on first use: a quarter of the logical processors, 2 to 4. Caller holds P.lock.
|
||||
static int workers_start(void) {
|
||||
if (A.threads) return 1;
|
||||
@@ -3407,9 +3409,10 @@ static int workers_start(void) {
|
||||
A.thread[A.threads++] = t; // kept: promote() raises a worker running a prebuild job
|
||||
}
|
||||
// Workers for stage libraries and fast links alone: two (the vertex and fragment libraries of one waiting draw in
|
||||
// parallel), and as many as the pool has with the fast links of skipped draws (lib_worker). They sleep unless a
|
||||
// library or a fast link is queued.
|
||||
int nlib = fast_workers() ? n : 2;
|
||||
// parallel), and as many as the pool has with the look-ahead, which queues the libraries of a whole submission's
|
||||
// new pipelines at once (vk_pipeline_hint), or with the fast links of skipped draws (lib_worker). They sleep
|
||||
// unless a library or a fast link is queued.
|
||||
int nlib = hint_on() || fast_workers() ? n : 2;
|
||||
for (int i = 0; A.threads && U.on && P.gpl && i < nlib; i++)
|
||||
if (plat_thread_new(lib_worker, NULL, "pipe lib worker")) U.lib_workers++;
|
||||
if (A.threads)
|
||||
@@ -3639,6 +3642,42 @@ static void lib_wait(const VkPipeState *s, const GKey *k, Layout *l, double end)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- stage libraries ahead of their draws (NX2NAVI_PIPE_AHEAD, default 1; 0 off; vk_exec.c "looking ahead")
|
||||
// The replay names the state of a draw it has not reached yet. When no pipeline is known for it, its pre-rasterization
|
||||
// and fragment shader libraries are queued to the library workers now, so they build while the draws before it are
|
||||
// recorded, in parallel with each other and with those of the other draws named: the draw's own lookup then finds both
|
||||
// and links on its thread in microseconds instead of waiting for them or being skipped while they build. Nothing else
|
||||
// is entered, no pipeline, no optimised build and no key record: a state the scan guessed wrong costs two libraries
|
||||
// at most. The feedback loop bits, the topology and the targets do not matter here, as no shader library depends on
|
||||
// them. Returns the libraries queued.
|
||||
static int hint_on(void) {
|
||||
static int on = -1;
|
||||
if (on < 0) on = cfg_bool("NX2NAVI_PIPE_AHEAD");
|
||||
return on;
|
||||
}
|
||||
|
||||
int vk_pipeline_hint(const VkPipeState *s) {
|
||||
if (!A.on || !U.on || !hint_on() || !s->vs || !ensure_init() || !P.gpl || !P.indep || is_mesh(s)) return 0;
|
||||
if (gfx_shader_stage(s->vs) != GFX_STAGE_VS || (s->fs && gfx_shader_stage(s->fs) != GFX_STAGE_FS)) return 0;
|
||||
GKey k;
|
||||
gkey_fill(s, &k);
|
||||
if (k.topo_class == 3) return 0;
|
||||
uint64_t h = key_hash(&k, sizeof k);
|
||||
int queued = 0;
|
||||
plat_mutex_lock(&P.lock);
|
||||
if (!pipe_find(&k, sizeof k, h)) {
|
||||
Bindings B = {0}, F = {0};
|
||||
Layout *l = add_stage(&B, s->vs, 0) && (!s->fs || add_stage(&F, s->fs, 0)) ? layout_pair(&B, &F) : NULL;
|
||||
Lib *b[2];
|
||||
uint64_t q0 = U.queued, w0 = U.waits;
|
||||
if (l && l->set1 && workers_start() && lib_queue(s, &k, l, b)) queued = (int)(U.queued - q0);
|
||||
U.waits = w0, U.queued = q0; // those count waiting draws' jobs
|
||||
P.g.hints++, P.g.hint_libs += (uint64_t)queued;
|
||||
}
|
||||
plat_mutex_unlock(&P.lock);
|
||||
return queued;
|
||||
}
|
||||
|
||||
static VkPipeline graphics(const VkPipeState *s, const VkPipeInfo **info, int async, int *pending, uint32_t wait_ms) {
|
||||
if (info) *info = NULL;
|
||||
if (pending) *pending = 0;
|
||||
@@ -5864,6 +5903,9 @@ static void save_with(uint32_t wait_ms) {
|
||||
(unsigned long long)P.g.ready_n[0], (unsigned long long)P.g.ready_n[1], (unsigned long long)P.g.ready_n[2],
|
||||
(unsigned long long)P.g.ready_n[3], (unsigned long long)P.g.ready_n[4], (unsigned long long)P.g.ready_n[5],
|
||||
rn ? P.g.ready_ms / (double)rn : 0.0, P.g.ready_max, (unsigned long long)P.g.ready_nofast);
|
||||
if (P.g.hints)
|
||||
rt_log("vk_pipeline: gpl: looking ahead named %llu pipeline states not known yet and queued %llu stage libraries "
|
||||
"for them", (unsigned long long)P.g.hints, (unsigned long long)P.g.hint_libs);
|
||||
if (U.waits || U.queued)
|
||||
rt_log("vk_pipeline: gpl: library jobs for waiting draws: %llu queued, %llu built by a worker, %llu by the "
|
||||
"thread that needed them (no worker free), %llu waits ended at their limit; %llu fast links on the %d "
|
||||
|
||||
Reference in New Issue
Block a user