Repository navigation
Compiling Vulkan shader code at registration, not at the first draw (VK_EXT_shader_object) #2242
Replies: 2 comments 22 replies
|
We ran into the same prerequisite from the performance side, in #1612. On Demon's Souls (PPSA01341) most of the per-draw CPU cost is resolving descriptors that the shader could read itself, and moving that resolution onto the GPU needs the same thing your step 1 needs: code that doesn't depend on descriptor contents at draw time. So I think this is one direction with two reasons for it: cold-start correctness here, and per-draw CPU cost there. Two data points from Demon's Souls on Linux (RTX 5070 Ti, NVIDIA 595.91.07), tutorial level:
On the vertex input question I don't have an answer. Our study left vertex fetch on the current fixed-function path and didn't measure either option. I can count how many vertex format combinations each vertex shader actually sees in Demon's Souls, which would show how many variants the "keep vertex input" option needs for this title. If it helps, I can test steps 1-3 on Demon's Souls on Linux/NVIDIA as they land, with before and after frame numbers. AI-assisted: yes |
|
Thanks for the write-up and the numbers. We're taking this on: all five steps, one PR each, with your measurements as the reference. Before choosing the mechanism, I measured it. Setup. I registered every
PS: today a 13 ms median and a 35.7 s worst case. GPL ahead takes 5.6 ms median and links in 10 µs. Shader objects take 18.7 ms median. Both approaches take the compile off the draw. On RADV, GPL also costs less ahead of time (the driver defers part of the work to the link), and it keeps Spec constants in those modules, which is what step 1 removes: buffer V# words in 680 of 780 modules, push data offset in 668, vertex formats in 548, image/heap selectors in 179, export mapping in 76, depth compare in 34. Buffers already have a runtime V# path ( What I can't measure here is NVIDIA. @Raghav314, could you run the same benchmark on your 4090? It's one file, with your Hades II modules as input, or any directory of Your BDA lookup change is independent. Please send it as you planned. AI-assisted: yes (Claude). All numbers are from real runs; raw TSVs on request. bench.cpp// Offline benchmark: cost at first draw of a graphics stage, three ways (RADV/any driver).
// mono: vkCreateGraphicsPipelines (what AnyPS5 does today, at the first draw)
// gpl : VK_EXT_graphics_pipeline_library, stage libraries built ahead, fast link at the draw (+ LTO link, background)
// so : VK_EXT_shader_object, vkCreateShadersEXT ahead, nothing at the draw
// Input: directory of NNNNN_<stage>_<addr>.spv dumped at registration. Stage under test is paired with a stub.
#include <vulkan/vulkan.h>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace fs = std::filesystem;
using Clock = std::chrono::steady_clock;
static double ms(Clock::time_point a, Clock::time_point b) { return std::chrono::duration<double, std::milli>(b - a).count(); }
#define CHECK(x) do { VkResult r_ = (x); if (r_ != VK_SUCCESS) { std::fprintf(stderr, "%s -> %d\n", #x, r_); throw r_; } } while (0)
struct Var { uint32_t location = ~0u, comps = 4, base = 0 /*0 f 1 i 2 u*/; bool flat = false; bool builtin = false; uint32_t arrayLen = 1; };
struct Desc { uint32_t set, binding, count; VkDescriptorType type; };
struct Module {
std::string name; std::vector<uint32_t> words; uint32_t model = ~0u; std::string entry;
std::vector<Var> inputs, outputs; std::vector<Desc> descs; bool push = false;
};
static Module Reflect(const std::string& name, std::vector<uint32_t> w) {
Module m; m.name = name;
std::map<uint32_t, std::vector<uint32_t>> types, consts; // id -> instruction words (from opcode)
std::map<uint32_t, uint32_t> set, binding, location, builtin; std::set<uint32_t> flat, block, bufferBlock;
std::vector<std::vector<uint32_t>> vars;
for (size_t c = 5; c < w.size();) {
uint32_t n = w[c] >> 16, op = w[c] & 0xffff; std::vector<uint32_t> ins(w.begin() + c, w.begin() + c + n);
if (op == 15 && m.model == ~0u) { m.model = ins[1]; m.entry = reinterpret_cast<const char*>(&ins[3]); }
else if (op == 71) {
uint32_t d = ins[2];
if (d == 34) set[ins[1]] = ins[3]; else if (d == 33) binding[ins[1]] = ins[3]; else if (d == 30) location[ins[1]] = ins[3];
else if (d == 11) builtin[ins[1]] = ins[3]; else if (d == 14) flat.insert(ins[1]); else if (d == 2) block.insert(ins[1]); else if (d == 3) bufferBlock.insert(ins[1]);
} else if (op == 72 && ins[3] == 11) builtin[ins[1]] = 1;
else if (op >= 19 && op <= 39 || op == 5341) types[ins[1]] = ins;
else if (op == 43 || op == 50) consts[ins[2]] = ins;
else if (op == 59) vars.push_back(ins);
c += n;
}
auto scalar = [&](uint32_t t, Var& v) {
auto ty = types[t];
if (ty[0] >> 16 && (ty[0] & 0xffff) == 28) { v.arrayLen = consts.count(ty[3]) ? consts[ty[3]][3] : 1; t = ty[2]; ty = types[t]; }
if ((ty[0] & 0xffff) == 23) { v.comps = ty[3]; t = ty[2]; ty = types[t]; } else v.comps = 1;
uint32_t op = ty[0] & 0xffff;
v.base = op == 22 ? 0 : (op == 21 && ty[3]) ? 1 : 2;
};
for (auto& ins : vars) {
uint32_t ptr = ins[1], id = ins[2], sc = ins[3]; uint32_t pointee = types[ptr][3];
if (sc == 1 || sc == 3) {
Var v; v.builtin = builtin.count(id) || (types[pointee][0] & 0xffff) == 30; v.flat = flat.count(id);
if (location.count(id)) v.location = location[id];
if (!v.builtin) scalar(pointee, v);
(sc == 1 ? m.inputs : m.outputs).push_back(v);
} else if (sc == 9) m.push = true;
else if (sc == 0 || sc == 2 || sc == 12) {
if (!binding.count(id)) continue;
uint32_t count = 1, t = pointee; auto ty = types[t];
if ((ty[0] & 0xffff) == 28) { count = consts.count(ty[3]) ? std::max(1u, consts[ty[3]][3]) : 1; t = ty[2]; }
else if ((ty[0] & 0xffff) == 29) { count = 64; t = ty[2]; }
ty = types[t]; uint32_t op = ty[0] & 0xffff; VkDescriptorType dt;
if (op == 25) { bool buf = ty[3] == 5; bool storage = ty[7] == 2; dt = buf ? (storage ? VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER : VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER) : (storage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE); }
else if (op == 26) dt = VK_DESCRIPTOR_TYPE_SAMPLER;
else if (op == 27) dt = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
else if (sc == 12 || bufferBlock.count(t)) dt = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
else dt = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
m.descs.push_back({set.count(id) ? set[id] : 0, binding[id], count, dt});
}
}
m.words = std::move(w);
return m;
}
static std::vector<uint32_t> Glsl(const std::string& src, const char* stage) {
static int k = 0; std::string base = (fs::temp_directory_path() / ("sobench_stub" + std::to_string(k++))).string();
std::ofstream(base + ".glsl") << src;
std::string cmd = std::string("glslc -O -fshader-stage=") + stage + " --target-env=vulkan1.3 " + base + ".glsl -o " + base + ".spv";
if (std::system(cmd.c_str()) != 0) { std::fprintf(stderr, "%s\n", src.c_str()); throw 1; }
std::ifstream in(base + ".spv", std::ios::binary); std::vector<char> b((std::istreambuf_iterator<char>(in)), {});
std::vector<uint32_t> out(b.size() / 4); std::memcpy(out.data(), b.data(), b.size()); return out;
}
static const char* GType(const Var& v, bool vec = true) {
static const char* t[3][4] = {{"float", "vec2", "vec3", "vec4"}, {"int", "ivec2", "ivec3", "ivec4"}, {"uint", "uvec2", "uvec3", "uvec4"}};
return t[v.base][(vec ? v.comps : 1) - 1];
}
static std::vector<uint32_t> StubVertexFor(const Module& fs) {
std::string s = "#version 450\n"; std::string body;
for (auto& v : fs.inputs) if (!v.builtin && v.location != ~0u) {
std::string nm = "o" + std::to_string(v.location);
s += "layout(location=" + std::to_string(v.location) + ") " + (v.flat || v.base ? "flat " : "") + "out " + GType(v) + " " + nm + (v.arrayLen > 1 ? "[" + std::to_string(v.arrayLen) + "]" : "") + ";\n";
if (v.arrayLen > 1) for (uint32_t i = 0; i < v.arrayLen; ++i) body += nm + "[" + std::to_string(i) + "] = " + GType(v) + "(gl_VertexIndex);\n";
else body += nm + " = " + GType(v) + "(gl_VertexIndex);\n";
}
return Glsl(s + "void main(){ gl_Position = vec4(float(gl_VertexIndex)); " + body + "}\n", "vert");
}
struct Vk {
VkInstance inst; VkPhysicalDevice phys; VkDevice dev; uint32_t qf = 0; std::string name;
PFN_vkCreateShadersEXT createShaders; PFN_vkDestroyShaderEXT destroyShader;
};
static Vk Open() {
Vk vk{}; VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.apiVersion = VK_API_VERSION_1_3;
VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; ici.pApplicationInfo = &app;
const char* layer = "VK_LAYER_KHRONOS_validation"; if (std::getenv("BENCH_VALIDATE")) { ici.enabledLayerCount = 1; ici.ppEnabledLayerNames = &layer; }
CHECK(vkCreateInstance(&ici, nullptr, &vk.inst));
uint32_t n = 0; vkEnumeratePhysicalDevices(vk.inst, &n, nullptr); std::vector<VkPhysicalDevice> ps(n); vkEnumeratePhysicalDevices(vk.inst, &n, ps.data());
int want = std::getenv("BENCH_GPU") ? std::atoi(std::getenv("BENCH_GPU")) : 0; vk.phys = ps.at(want);
VkPhysicalDeviceProperties pp; vkGetPhysicalDeviceProperties(vk.phys, &pp); vk.name = pp.deviceName;
VkPhysicalDeviceShaderObjectFeaturesEXT so{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_OBJECT_FEATURES_EXT};
VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT gpl{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &so};
VkPhysicalDeviceVulkan13Features f13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, &gpl};
VkPhysicalDeviceVulkan12Features f12{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, &f13};
VkPhysicalDeviceVulkan11Features f11{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, &f12};
VkPhysicalDeviceFeatures2 f2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &f11};
vkGetPhysicalDeviceFeatures2(vk.phys, &f2);
f2.features.robustBufferAccess = VK_FALSE;
uint32_t en = 0; vkEnumerateDeviceExtensionProperties(vk.phys, nullptr, &en, nullptr); std::vector<VkExtensionProperties> ex(en); vkEnumerateDeviceExtensionProperties(vk.phys, nullptr, &en, ex.data());
std::set<std::string> have; for (auto& e : ex) have.insert(e.extensionName);
std::vector<const char*> exts;
for (const char* e : {"VK_EXT_shader_object", "VK_EXT_graphics_pipeline_library", "VK_KHR_pipeline_library", "VK_KHR_fragment_shader_barycentric", "VK_EXT_shader_image_atomic_int64",
"VK_EXT_shader_atomic_float", "VK_KHR_shader_clock", "VK_EXT_fragment_shader_interlock", "VK_KHR_shader_maximal_reconvergence", "VK_KHR_shader_float_controls2",
"VK_KHR_shader_subgroup_uniform_control_flow", "VK_EXT_shader_stencil_export", "VK_AMD_shader_ballot", "VK_KHR_shader_relaxed_extended_instruction", "VK_EXT_shader_replicated_composites"})
if (have.count(e)) exts.push_back(e);
if (!so.shaderObject || !gpl.graphicsPipelineLibrary) { std::fprintf(stderr, "missing shaderObject/GPL\n"); std::exit(2); }
// Enable every core/known feature the device reports, plus the extension feature structs we enable.
void* tail = nullptr; std::vector<std::vector<char>> keep;
auto chain = [&](VkStructureType t, size_t size, const char* ext) {
if (!have.count(ext)) return;
std::vector<char> b(size, 0); auto* h = reinterpret_cast<VkBaseOutStructure*>(b.data()); h->sType = t;
VkPhysicalDeviceFeatures2 q{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, h}; vkGetPhysicalDeviceFeatures2(vk.phys, &q);
h->pNext = static_cast<VkBaseOutStructure*>(tail); keep.push_back(std::move(b)); tail = keep.back().data();
};
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR, sizeof(VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR), "VK_KHR_fragment_shader_barycentric");
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_IMAGE_ATOMIC_INT64_FEATURES_EXT, sizeof(VkPhysicalDeviceShaderImageAtomicInt64FeaturesEXT), "VK_EXT_shader_image_atomic_int64");
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_FLOAT_FEATURES_EXT, sizeof(VkPhysicalDeviceShaderAtomicFloatFeaturesEXT), "VK_EXT_shader_atomic_float");
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR, sizeof(VkPhysicalDeviceShaderClockFeaturesKHR), "VK_KHR_shader_clock");
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT, sizeof(VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT), "VK_EXT_fragment_shader_interlock");
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_MAXIMAL_RECONVERGENCE_FEATURES_KHR, sizeof(VkPhysicalDeviceShaderMaximalReconvergenceFeaturesKHR), "VK_KHR_shader_maximal_reconvergence");
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT_CONTROLS_2_FEATURES_KHR, sizeof(VkPhysicalDeviceShaderFloatControls2FeaturesKHR), "VK_KHR_shader_float_controls2");
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SUBGROUP_UNIFORM_CONTROL_FLOW_FEATURES_KHR, sizeof(VkPhysicalDeviceShaderSubgroupUniformControlFlowFeaturesKHR), "VK_KHR_shader_subgroup_uniform_control_flow");
chain(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_RELAXED_EXTENDED_INSTRUCTION_FEATURES_KHR, sizeof(VkPhysicalDeviceShaderRelaxedExtendedInstructionFeaturesKHR), "VK_KHR_shader_relaxed_extended_instruction");
so.pNext = tail;
float prio = 1; VkDeviceQueueCreateInfo q{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; q.queueCount = 1; q.pQueuePriorities = &prio;
VkDeviceCreateInfo dci{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &f2}; dci.queueCreateInfoCount = 1; dci.pQueueCreateInfos = &q;
dci.enabledExtensionCount = exts.size(); dci.ppEnabledExtensionNames = exts.data();
CHECK(vkCreateDevice(vk.phys, &dci, nullptr, &vk.dev));
vk.createShaders = (PFN_vkCreateShadersEXT)vkGetDeviceProcAddr(vk.dev, "vkCreateShadersEXT");
vk.destroyShader = (PFN_vkDestroyShaderEXT)vkGetDeviceProcAddr(vk.dev, "vkDestroyShaderEXT");
return vk;
}
struct Layout { std::vector<VkDescriptorSetLayout> sets; VkPipelineLayout pipe; VkPushConstantRange push; };
static Layout MakeLayout(Vk& vk, const Module& a, const Module& b) {
std::map<uint32_t, std::map<uint32_t, VkDescriptorSetLayoutBinding>> sets;
for (auto* m : {&a, &b}) for (auto& d : m->descs) {
auto& bnd = sets[d.set][d.binding]; bnd.binding = d.binding; bnd.descriptorType = d.type; bnd.descriptorCount = std::max(bnd.descriptorCount, d.count); bnd.stageFlags = VK_SHADER_STAGE_ALL_GRAPHICS;
}
Layout l{}; uint32_t maxSet = sets.empty() ? 0 : sets.rbegin()->first + 1;
for (uint32_t s = 0; s < maxSet; ++s) {
std::vector<VkDescriptorSetLayoutBinding> bs; for (auto& [k, v] : sets[s]) bs.push_back(v);
std::vector<VkDescriptorBindingFlags> fl(bs.size(), VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT);
VkDescriptorSetLayoutBindingFlagsCreateInfo bf{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; bf.bindingCount = fl.size(); bf.pBindingFlags = fl.data();
VkDescriptorSetLayoutCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, &bf}; ci.bindingCount = bs.size(); ci.pBindings = bs.data();
VkDescriptorSetLayout sl; CHECK(vkCreateDescriptorSetLayout(vk.dev, &ci, nullptr, &sl)); l.sets.push_back(sl);
}
l.push = {VK_SHADER_STAGE_ALL_GRAPHICS, 0, 256};
VkPipelineLayoutCreateInfo pci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; pci.setLayoutCount = l.sets.size(); pci.pSetLayouts = l.sets.data(); pci.pushConstantRangeCount = 1; pci.pPushConstantRanges = &l.push;
CHECK(vkCreatePipelineLayout(vk.dev, &pci, nullptr, &l.pipe));
return l;
}
static VkFormat AttribFormat(const Var& v) {
static const VkFormat f[3][4] = {{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT},
{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT, VK_FORMAT_R32G32B32A32_SINT},
{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT, VK_FORMAT_R32G32B32A32_UINT}};
return f[v.base][v.comps - 1];
}
static VkFormat ColorFormat(const Var& v) { return v.base == 0 ? VK_FORMAT_R16G16B16A16_SFLOAT : v.base == 1 ? VK_FORMAT_R32G32B32A32_SINT : VK_FORMAT_R32G32B32A32_UINT; }
struct Pair { const Module* vs; const Module* fs; };
struct State {
std::vector<VkVertexInputBindingDescription> bindings; std::vector<VkVertexInputAttributeDescription> attrs; std::vector<VkFormat> colors;
VkPipelineVertexInputStateCreateInfo vi{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
VkPipelineViewportStateCreateInfo vp{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
VkPipelineRasterizationStateCreateInfo rs{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
VkPipelineDepthStencilStateCreateInfo ds{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
std::vector<VkPipelineColorBlendAttachmentState> blends; VkPipelineColorBlendStateCreateInfo cb{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dy{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
VkPipelineRenderingCreateInfo ri{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO};
explicit State(const Pair& p) {
for (auto& v : p.vs->inputs) if (!v.builtin && v.location != ~0u) {
bindings.push_back({(uint32_t)bindings.size(), 64, VK_VERTEX_INPUT_RATE_VERTEX});
attrs.push_back({v.location, bindings.back().binding, AttribFormat(v), 0});
}
for (auto& v : p.fs->outputs) if (!v.builtin && v.location != ~0u) { if (colors.size() <= v.location) colors.resize(v.location + 1, VK_FORMAT_UNDEFINED); colors[v.location] = ColorFormat(v); }
vi.vertexBindingDescriptionCount = bindings.size(); vi.pVertexBindingDescriptions = bindings.data(); vi.vertexAttributeDescriptionCount = attrs.size(); vi.pVertexAttributeDescriptions = attrs.data();
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; vp.viewportCount = vp.scissorCount = 1;
rs.lineWidth = 1; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
blends.resize(colors.size()); for (auto& b : blends) b.colorWriteMask = 0xf;
cb.attachmentCount = blends.size(); cb.pAttachments = blends.data();
dy.dynamicStateCount = 2; dy.pDynamicStates = dyn;
ri.colorAttachmentCount = colors.size(); ri.pColorAttachmentFormats = colors.data(); ri.depthAttachmentFormat = VK_FORMAT_D32_SFLOAT;
}
};
static VkShaderModule ModuleOf(Vk& vk, const Module& m) {
VkShaderModuleCreateInfo ci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; ci.codeSize = m.words.size() * 4; ci.pCode = m.words.data();
VkShaderModule s; CHECK(vkCreateShaderModule(vk.dev, &ci, nullptr, &s)); return s;
}
static VkPipelineShaderStageCreateInfo StageOf(VkShaderStageFlagBits st, VkShaderModule m, const Module& mod) {
VkPipelineShaderStageCreateInfo s{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; s.stage = st; s.module = m; s.pName = mod.entry.c_str(); return s;
}
struct Result { double mono = -1, gplVs = -1, gplFs = -1, gplVi = -1, gplFo = -1, gplLink = -1, gplLto = -1, soVs = -1, soFs = -1; std::string error; };
static Result Run(Vk& vk, const Pair& p) {
Result r; State st(p); Layout l = MakeLayout(vk, *p.vs, *p.fs);
VkShaderModule vsm = ModuleOf(vk, *p.vs), fsm = ModuleOf(vk, *p.fs);
VkPipelineShaderStageCreateInfo stages[2] = {StageOf(VK_SHADER_STAGE_VERTEX_BIT, vsm, *p.vs), StageOf(VK_SHADER_STAGE_FRAGMENT_BIT, fsm, *p.fs)};
const std::string mode = std::getenv("BENCH_MODE") ? std::getenv("BENCH_MODE") : "all";
try {
if (mode == "all" || mode == "mono") {
VkGraphicsPipelineCreateInfo g{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &st.ri};
g.stageCount = 2; g.pStages = stages; g.pVertexInputState = &st.vi; g.pInputAssemblyState = &st.ia; g.pViewportState = &st.vp; g.pRasterizationState = &st.rs;
g.pMultisampleState = &st.ms; g.pDepthStencilState = &st.ds; g.pColorBlendState = &st.cb; g.pDynamicState = &st.dy; g.layout = l.pipe;
VkPipeline mono; auto t0 = Clock::now(); CHECK(vkCreateGraphicsPipelines(vk.dev, VK_NULL_HANDLE, 1, &g, nullptr, &mono)); r.mono = ms(t0, Clock::now());
vkDestroyPipeline(vk.dev, mono, nullptr);
}
if (mode == "all" || mode == "gpl") {
// gpl: four libraries ahead, link at the draw
auto lib = [&](VkGraphicsPipelineLibraryFlagsEXT part, double& out) {
VkGraphicsPipelineLibraryCreateInfoEXT li{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, &st.ri}; li.flags = part;
VkGraphicsPipelineCreateInfo c{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &li};
c.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT; c.layout = l.pipe;
if (part == VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT) { c.pVertexInputState = &st.vi; c.pInputAssemblyState = &st.ia; }
if (part == VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT) { c.stageCount = 1; c.pStages = &stages[0]; c.pViewportState = &st.vp; c.pRasterizationState = &st.rs; c.pDynamicState = &st.dy; }
if (part == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT) { c.stageCount = 1; c.pStages = &stages[1]; c.pMultisampleState = &st.ms; c.pDepthStencilState = &st.ds; }
if (part == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT) { c.pMultisampleState = &st.ms; c.pColorBlendState = &st.cb; }
VkPipeline p_; auto t = Clock::now(); CHECK(vkCreateGraphicsPipelines(vk.dev, VK_NULL_HANDLE, 1, &c, nullptr, &p_)); out = ms(t, Clock::now()); return p_;
};
VkPipeline libs[4] = {lib(VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT, r.gplVi), lib(VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT, r.gplVs),
lib(VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT, r.gplFs), lib(VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT, r.gplFo)};
auto link = [&](bool lto, double& out) {
VkPipelineLibraryCreateInfoKHR li{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; li.libraryCount = 4; li.pLibraries = libs;
VkGraphicsPipelineCreateInfo c{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &li}; c.layout = l.pipe; if (lto) c.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT;
VkPipeline p_; auto t = Clock::now(); CHECK(vkCreateGraphicsPipelines(vk.dev, VK_NULL_HANDLE, 1, &c, nullptr, &p_)); out = ms(t, Clock::now()); vkDestroyPipeline(vk.dev, p_, nullptr);
};
link(false, r.gplLink); link(true, r.gplLto);
for (auto x : libs) vkDestroyPipeline(vk.dev, x, nullptr);
}
if (mode == "all" || mode == "so") {
// shader objects, unlinked, created one by one (as registration would)
auto obj = [&](const Module& m, VkShaderStageFlagBits stage, VkShaderStageFlags next, double& out) {
VkShaderCreateInfoEXT c{VK_STRUCTURE_TYPE_SHADER_CREATE_INFO_EXT}; c.stage = stage; c.nextStage = next; c.codeType = VK_SHADER_CODE_TYPE_SPIRV_EXT;
c.codeSize = m.words.size() * 4; c.pCode = m.words.data(); c.pName = m.entry.c_str(); c.setLayoutCount = l.sets.size(); c.pSetLayouts = l.sets.data();
c.pushConstantRangeCount = 1; c.pPushConstantRanges = &l.push;
VkShaderEXT s; auto t = Clock::now(); CHECK(vk.createShaders(vk.dev, 1, &c, nullptr, &s)); out = ms(t, Clock::now()); vk.destroyShader(vk.dev, s, nullptr);
};
obj(*p.vs, VK_SHADER_STAGE_VERTEX_BIT, VK_SHADER_STAGE_FRAGMENT_BIT, r.soVs); obj(*p.fs, VK_SHADER_STAGE_FRAGMENT_BIT, 0, r.soFs);
}
} catch (VkResult e) { r.error = "VkResult " + std::to_string(e); }
vkDestroyShaderModule(vk.dev, vsm, nullptr); vkDestroyShaderModule(vk.dev, fsm, nullptr);
vkDestroyPipelineLayout(vk.dev, l.pipe, nullptr); for (auto s : l.sets) vkDestroyDescriptorSetLayout(vk.dev, s, nullptr);
return r;
}
int main(int argc, char** argv) {
std::vector<Module> mods;
std::vector<fs::path> files; for (auto& e : fs::directory_iterator(argv[1])) if (e.path().extension() == ".spv") files.push_back(e.path());
std::sort(files.begin(), files.end());
std::set<std::vector<uint32_t>> unique;
for (auto& f : files) {
std::ifstream in(f, std::ios::binary); std::vector<char> b((std::istreambuf_iterator<char>(in)), {});
std::vector<uint32_t> w(b.size() / 4); std::memcpy(w.data(), b.data(), b.size());
if (w.size() < 5 || !unique.insert(w).second) continue;
auto m = Reflect(f.filename().string(), std::move(w)); if (m.model == 0 || m.model == 4) mods.push_back(std::move(m));
}
const size_t limit = argc > 2 ? std::atoi(argv[2]) : mods.size();
Vk vk = Open(); std::fprintf(stderr, "device %s, %zu unique graphics modules\n", vk.name.c_str(), mods.size());
Module stubFs = Reflect("stubfs", Glsl("#version 450\nvoid main(){}\n", "frag"));
std::map<std::string, Module> stubVs;
std::printf("name\tstage\twords\tmono\tgpl_vi\tgpl_vs\tgpl_fs\tgpl_fo\tgpl_link\tgpl_lto\tso_vs\tso_fs\terror\n");
size_t done = 0;
for (auto& m : mods) {
if (done++ >= limit) break;
Pair p{};
if (m.model == 0) p = {&m, &stubFs};
else {
std::string key; for (auto& v : m.inputs) if (!v.builtin) key += std::to_string(v.location) + ":" + std::to_string(v.base) + std::to_string(v.comps) + std::to_string(v.flat) + "x" + std::to_string(v.arrayLen) + ",";
if (!stubVs.count(key)) stubVs.emplace(key, Reflect("stubvs", StubVertexFor(m)));
p = {&stubVs.at(key), &m};
}
Result r = Run(vk, p);
std::printf("%s\t%s\t%zu\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t%s\n", m.name.c_str(), m.model == 0 ? "vs" : "fs", m.words.size(),
r.mono, r.gplVi, r.gplVs, r.gplFs, r.gplFo, r.gplLink, r.gplLto, r.soVs, r.soFs, r.error.c_str());
std::fflush(stdout);
}
} |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Update: the maintainers went with
VK_EXT_graphics_pipeline_libraryinstead of shader objects (see adriadam10's reply and the 4090 numbers under it). Phase A landed in #2636; phase B (building the libraries at registration) is next.Hey @boykopovar, I've been chasing a startup crash in Hades II (PPSA36082) and it led somewhere bigger than one fix, so I'd rather talk it through before writing a huge PR.
The problem
Hades II only boots when the shader caches are warm. On a cold start (first launch, or the first launch after any recompiler change, because the disk cache directory is keyed on a hash of the recompiler sources) it dies 3.2-3.5 s in, every time I've tried.
What happens: the game's loader stages data into a 512 MiB pool and only reuses pool memory once a GPU fence (a RELEASE_MEM on the graphics queue) has landed. On a PS5 that fence comes back within a few ms. Here, the queue worker hits a draw whose pipeline isn't compiled yet and sits in
vkCreateGraphicsPipelinesfor ~65 ms. The fence is on the same queue behind that frame, so it's correctly ordered after the draw and just can't land in time. The loader keeps staging, the pool hits 512 MiB, and the game crashes.Numbers from my machine (RTX 4090, NVIDIA 615.71, Linux):
VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BITdoesn't help, NVIDIA ignores it (87 ms either way on a test shader).So as long as the driver compiles shader code when a draw first needs it, a cold start puts tens of ms of host work on the GPU timeline at a moment the game assumes is basically free.
Why I think the fix belongs in the architecture
#639 already says where this should go:
The RDNA to SPIR-V step already happens at registration. The NVIDIA/AMD compile of that SPIR-V is the one compile left at draw time, and it's the expensive one.
Vulkan has a direct way to do this:
VK_EXT_shader_object. Each stage is compiled into aVkShaderEXTon its own, and a draw only binds the stage objects and sets render state as dynamic commands. There's no pipeline object, so nothing compiles at draw. It also maps more closely onto how the PS5 works: precompiled per-stage code, and a draw is register state plus "go".I checked it with the actual Hades shaders, offline, with the validation layer on (zero messages):
What would have to change
Here's the catch, and the reason I'm asking first. To build a
VkShaderEXTat registration, the stage's code can't depend on anything that's only known at draw time. Today four things are folded in per draw (materializeResult/buildSpecializedModuleinRecompiler.cpp):OpConstants. In the generic modules I looked at, these are only used as selectors in code that already handles every case (if format == 1 / 4 / 5 …), so they could become values read from a small per-draw parameter block. The one structural use is the bindless heap arrays, whose length is a spec constant (HeapCountBase); those would become runtime-sized arrays.SpecializeVertexInputTypesrewrites the shader's input variable types (float, int or uint) to match each draw's V# format. A shader object needs fixed types. The faithful option is fetching vertices in the shader from the V#, which is what the hardware does, but it replaces the current Vulkan vertex-input path.preparedBindingPlan). A shader object fixes its set layouts at creation, so each stage needs a layout that's stable from registration on.pushConstantOffsetBytesis in the module key). It would need a fixed place per stage.Plus dynamic rendering instead of
VkRenderPassfor this path, every bit of render state as dynamic commands (extended dynamic state 1-3, vertex input dynamic state), and the same for compute. Devices withoutVK_EXT_shader_objectwould keep today's pipeline path as a fallback. NVIDIA ships the extension natively, and RADV has had it enabled by default since Mesa 24.1.How I'd split it
Every step stands on its own and keeps today's path working:
The BDA lookup change is independent and can go in whenever.
Questions
Happy to share the measurement harness and the dumped Hades shaders if you want to reproduce the numbers.
Full disclosure: I used an LLM (Claude) to help with this investigation: profiling, writing the offline test harness, reading through the recompiler and drafting this post. All the numbers come from real runs on my machine.
All reactions