/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2018-2019 Baldur Karlsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. ******************************************************************************/ #include #include "3rdparty/glslang/SPIRV/GLSL.std.450.h" #include "3rdparty/glslang/SPIRV/spirv.hpp" #include "driver/shaders/spirv/spirv_editor.h" #include "vk_core.h" #include "vk_debug.h" #include "vk_shader_cache.h" struct VkXfbQueryResult { uint64_t numPrimitivesWritten; uint64_t numPrimitivesGenerated; }; static const char *PatchedMeshOutputEntryPoint = "rdc"; static const uint32_t MeshOutputDispatchWidth = 128; static const uint32_t MeshOutputTBufferArraySize = 16; // 0 = output // 1 = indices // 2 = float vbuffers // 3 = uint vbuffers // 4 = sint vbuffers static const uint32_t MeshOutputReservedBindings = 5; static void ConvertToMeshOutputCompute(const ShaderReflection &refl, const SPIRVPatchData &patchData, const char *entryName, std::vector instDivisor, const DrawcallDescription *draw, uint32_t numVerts, uint32_t numViews, std::vector &modSpirv, uint32_t &bufStride) { SPIRVEditor editor(modSpirv); uint32_t numInputs = (uint32_t)refl.inputSignature.size(); uint32_t numOutputs = (uint32_t)refl.outputSignature.size(); RDCASSERT(numOutputs > 0); for(rdcspv::Iter it = editor.Begin(SPIRVSection::Annotations), end = editor.End(SPIRVSection::Annotations); it < end; ++it) { // we will use descriptor set 0 bindings 0..N for our own purposes. // // Since bindings are arbitrary, we just increase all user bindings to make room, and we'll // redeclare the descriptor set layouts and pipeline layout. This is inevitable in the case // where all descriptor sets are already used. In theory we only have to do this with set 0, but // that requires knowing which variables are in set 0 and it's simpler to increase all bindings. if(it.opcode() == spv::OpDecorate && it.word(2) == spv::DecorationBinding) { RDCASSERT(it.word(3) < (0xffffffff - MeshOutputReservedBindings)); it.word(3) += MeshOutputReservedBindings; } } // tbuffer types, the values are the descriptor bindings enum tbufferType { tbuffer_undefined, tbuffer_float = 2, tbuffer_uint = 3, tbuffer_sint = 4, tbuffer_count, }; struct inputOutputIDs { // if this is a builtin value, what builtin value is expected ShaderBuiltin builtin = ShaderBuiltin::Undefined; // ID of the variable rdcspv::Id variableID; // constant ID for the index of this attribute rdcspv::Id constID; // the type ID for this attribute. Must be present already by definition! rdcspv::Id basetypeID; // tbuffer type for this input tbufferType tbuffer; // gvec4 type for this input, used as result type when fetching from tbuffer rdcspv::Id vec4ID; // Uniform Pointer ID for this output. Used only for output data, to write to output SSBO rdcspv::Id uniformPtrID; // Output Pointer ID for this attribute. // For inputs, used to 'write' to the global at the start. // For outputs, used to 'read' from the global at the end. rdcspv::Id privatePtrID; }; std::vector ins; ins.resize(numInputs); std::vector outs; outs.resize(numOutputs); std::set inputs; std::set outputs; std::map typeReplacements; // rewrite any inputs and outputs to be private storage class for(rdcspv::Iter it = editor.Begin(SPIRVSection::TypesVariablesConstants), end = editor.End(SPIRVSection::TypesVariablesConstants); it < end; ++it) { // rewrite any input/output variables to private, and build up inputs/outputs list if(it.opcode() == spv::OpTypePointer) { rdcspv::Id id; if(it.word(2) == spv::StorageClassInput) { id = rdcspv::Id::fromWord(it.word(1)); inputs.insert(id); } else if(it.word(2) == spv::StorageClassOutput) { id = rdcspv::Id::fromWord(it.word(1)); outputs.insert(id); rdcspv::Id baseId = rdcspv::Id::fromWord(it.word(3)); rdcspv::Iter baseIt = editor.GetID(baseId); if(baseIt && baseIt.opcode() == spv::OpTypeStruct) outputs.insert(baseId); } if(id) { SPIRVPointer privPtr(rdcspv::Id::fromWord(it.word(3)), spv::StorageClassPrivate); rdcspv::Id origId = editor.GetType(privPtr); if(origId) { // if we already had a private pointer for this type, we have to use that type - we can't // create a new type by aliasing. Thus we need to replace any uses of 'id' with 'origId'. typeReplacements[id] = origId; // and remove this type declaration editor.Remove(it); } else { editor.PreModify(it); it.word(2) = spv::StorageClassPrivate; // if we didn't already have this pointer, process the modified type declaration editor.PostModify(it); } } } else if(it.opcode() == spv::OpVariable) { bool mod = false; if(it.word(3) == spv::StorageClassInput) { mod = true; editor.PreModify(it); it.word(3) = spv::StorageClassPrivate; inputs.insert(rdcspv::Id::fromWord(it.word(2))); } else if(it.word(3) == spv::StorageClassOutput) { mod = true; editor.PreModify(it); it.word(3) = spv::StorageClassPrivate; outputs.insert(rdcspv::Id::fromWord(it.word(2))); } auto replIt = typeReplacements.find(rdcspv::Id::fromWord(it.word(1))); if(replIt != typeReplacements.end()) { if(!mod) editor.PreModify(it); mod = true; it.word(1) = typeReplacements[rdcspv::Id::fromWord(it.word(1))].value(); } if(mod) editor.PostModify(it); // if we repointed this variable to an existing private declaration, we must also move it to // the end of the section. The reason being that the private pointer type declared may be // declared *after* this variable. There can't be any dependencies on this later in the // section because it's a variable not a type, so it's safe to move to the end. if(replIt != typeReplacements.end()) { // make a copy of the opcode rdcspv::Operation op = rdcspv::Operation::copy(it); // remove the old one editor.Remove(it); // add it anew editor.AddVariable(op); } } else if(it.opcode() == spv::OpTypeFunction) { bool mod = false; auto replIt = typeReplacements.find(rdcspv::Id::fromWord(it.word(1))); if(replIt != typeReplacements.end()) { editor.PreModify(it); mod = true; it.word(1) = typeReplacements[rdcspv::Id::fromWord(it.word(1))].value(); } for(size_t i = 4; i < it.size(); it++) { replIt = typeReplacements.find(rdcspv::Id::fromWord(it.word(i))); if(replIt != typeReplacements.end()) { if(!mod) editor.PreModify(it); mod = true; it.word(i) = typeReplacements[rdcspv::Id::fromWord(it.word(i))].value(); } } if(mod) editor.PostModify(it); } else if(it.opcode() == spv::OpConstantNull) { auto replIt = typeReplacements.find(rdcspv::Id::fromWord(it.word(1))); if(replIt != typeReplacements.end()) { editor.PreModify(it); it.word(1) = typeReplacements[rdcspv::Id::fromWord(it.word(1))].value(); editor.PostModify(it); } } } for(rdcspv::Iter it = editor.Begin(SPIRVSection::Functions); it; ++it) { // identify functions with result types we might want to replace if(it.opcode() == spv::OpFunction || it.opcode() == spv::OpFunctionParameter || it.opcode() == spv::OpVariable || it.opcode() == spv::OpAccessChain || it.opcode() == spv::OpInBoundsAccessChain || it.opcode() == spv::OpBitcast || it.opcode() == spv::OpUndef || it.opcode() == spv::OpExtInst || it.opcode() == spv::OpFunctionCall || it.opcode() == spv::OpPhi) { editor.PreModify(it); uint32_t &id = it.word(1); auto replIt = typeReplacements.find(rdcspv::Id::fromWord(id)); if(replIt != typeReplacements.end()) id = typeReplacements[rdcspv::Id::fromWord(id)].value(); editor.PostModify(it); } } // detect builtin inputs or outputs, and remove builtin decorations for(rdcspv::Iter it = editor.Begin(SPIRVSection::Annotations), end = editor.End(SPIRVSection::Annotations); it < end; ++it) { // remove any builtin decorations if(it.opcode() == spv::OpDecorate && it.word(2) == spv::DecorationBuiltIn) { // we don't have to do anything, the ID mapping is in the SPIRVPatchData, so just discard the // location information editor.Remove(it); } if(it.opcode() == spv::OpMemberDecorate && it.word(3) == spv::DecorationBuiltIn) editor.Remove(it); // remove block decoration from input or output structs if(it.opcode() == spv::OpDecorate && it.word(2) == spv::DecorationBlock) { rdcspv::Id id = rdcspv::Id::fromWord(it.word(1)); if(outputs.find(id) != outputs.end() || inputs.find(id) != inputs.end()) editor.Remove(it); } // remove all invariant decoreations if(it.opcode() == spv::OpDecorate && it.word(2) == spv::DecorationInvariant) { editor.Remove(it); } if(it.opcode() == spv::OpDecorate && it.word(2) == spv::DecorationLocation) { // we don't have to do anything, the ID mapping is in the SPIRVPatchData, so just discard the // location information editor.Remove(it); } } rdcspv::Id entryID; std::set entries; for(const SPIRVEntry &entry : editor.GetEntries()) { if(entry.name == entryName) entryID = entry.id; entries.insert(entry.id); } RDCASSERT(entryID); for(rdcspv::Iter it = editor.Begin(SPIRVSection::Debug), end2 = editor.End(SPIRVSection::Debug); it < end2; ++it) { if(it.opcode() == spv::OpName && (inputs.find(rdcspv::Id::fromWord(it.word(1))) != inputs.end() || outputs.find(rdcspv::Id::fromWord(it.word(1))) != outputs.end())) { rdcspv::Id id = rdcspv::Id::fromWord(it.word(1)); std::string oldName = (const char *)&it.word(2); editor.Remove(it); if(typeReplacements.find(id) == typeReplacements.end()) editor.SetName(id, ("emulated_" + oldName).c_str()); } // remove any OpName for the old entry points if(it.opcode() == spv::OpName && entries.find(rdcspv::Id::fromWord(it.word(1))) != entries.end()) editor.Remove(it); } // declare necessary variables per-output, types and constants. We do this last so that we don't // add a private pointer that we later try and deduplicate when collapsing output/input pointers // to private for(uint32_t i = 0; i < numOutputs; i++) { inputOutputIDs &io = outs[i]; io.builtin = refl.outputSignature[i].systemValue; // constant for this index io.constID = editor.AddConstantImmediate(i); io.variableID = rdcspv::Id::fromWord(patchData.outputs[i].ID); // base type - either a scalar or a vector, since matrix outputs are decayed to vectors { SPIRVScalar scalarType = scalar(); if(refl.outputSignature[i].compType == CompType::UInt) scalarType = scalar(); else if(refl.outputSignature[i].compType == CompType::SInt) scalarType = scalar(); else if(refl.outputSignature[i].compType == CompType::Float) scalarType = scalar(); else if(refl.outputSignature[i].compType == CompType::Double) scalarType = scalar(); io.vec4ID = editor.DeclareType(SPIRVVector(scalarType, 4)); if(refl.outputSignature[i].compCount > 1) io.basetypeID = editor.DeclareType(SPIRVVector(scalarType, refl.outputSignature[i].compCount)); else io.basetypeID = editor.DeclareType(scalarType); } io.uniformPtrID = editor.DeclareType(SPIRVPointer(io.basetypeID, spv::StorageClassUniform)); io.privatePtrID = editor.DeclareType(SPIRVPointer(io.basetypeID, spv::StorageClassPrivate)); RDCASSERT(io.basetypeID && io.vec4ID && io.constID && io.privatePtrID && io.uniformPtrID, io.basetypeID, io.vec4ID, io.constID, io.privatePtrID, io.uniformPtrID); } // repeat for inputs for(uint32_t i = 0; i < numInputs; i++) { inputOutputIDs &io = ins[i]; io.builtin = refl.inputSignature[i].systemValue; // constant for this index io.constID = editor.AddConstantImmediate(i); io.variableID = rdcspv::Id::fromWord(patchData.inputs[i].ID); SPIRVScalar scalarType = scalar(); // base type - either a scalar or a vector, since matrix outputs are decayed to vectors if(refl.inputSignature[i].compType == CompType::UInt) { scalarType = scalar(); io.tbuffer = tbuffer_uint; } else if(refl.inputSignature[i].compType == CompType::SInt) { scalarType = scalar(); io.tbuffer = tbuffer_sint; } else if(refl.inputSignature[i].compType == CompType::Float) { scalarType = scalar(); io.tbuffer = tbuffer_float; } else if(refl.inputSignature[i].compType == CompType::Double) { scalarType = scalar(); // doubles are loaded packed from a uint tbuffer io.tbuffer = tbuffer_uint; } // doubles are loaded as uvec4 and then packed in pairs, so we need to declare vec4ID as uvec4 if(refl.inputSignature[i].compType == CompType::Double) io.vec4ID = editor.DeclareType(SPIRVVector(scalar(), 4)); else io.vec4ID = editor.DeclareType(SPIRVVector(scalarType, 4)); if(refl.inputSignature[i].compCount > 1) io.basetypeID = editor.DeclareType(SPIRVVector(scalarType, refl.inputSignature[i].compCount)); else io.basetypeID = editor.DeclareType(scalarType); io.privatePtrID = editor.DeclareType(SPIRVPointer(io.basetypeID, spv::StorageClassPrivate)); RDCASSERT(io.basetypeID && io.vec4ID && io.constID && io.privatePtrID, io.basetypeID, io.vec4ID, io.constID, io.privatePtrID); } struct tbufferIDs { rdcspv::Id imageTypeID; rdcspv::Id imageSampledTypeID; rdcspv::Id pointerTypeID; rdcspv::Id variableID; } tbuffers[tbuffer_count]; rdcspv::Id arraySize = editor.AddConstantImmediate(MeshOutputTBufferArraySize); for(tbufferType tb : {tbuffer_float, tbuffer_sint, tbuffer_uint}) { SPIRVScalar scalarType = scalar(); const char *name = "float_vbuffers"; if(tb == tbuffer_sint) { scalarType = scalar(); name = "int_vbuffers"; } else if(tb == tbuffer_uint) { scalarType = scalar(); name = "uint_vbuffers"; } tbuffers[tb].imageTypeID = editor.DeclareType( SPIRVImage(scalarType, spv::DimBuffer, 0, 0, 0, 1, spv::ImageFormatUnknown)); tbuffers[tb].imageSampledTypeID = editor.DeclareType(SPIRVSampledImage(tbuffers[tb].imageTypeID)); rdcspv::Id arrayType = editor.MakeId(); editor.AddType(rdcspv::Operation( spv::OpTypeArray, {arrayType.value(), tbuffers[tb].imageSampledTypeID.value(), arraySize.value()})); rdcspv::Id arrayPtrType = editor.DeclareType(SPIRVPointer(arrayType, spv::StorageClassUniformConstant)); tbuffers[tb].pointerTypeID = editor.DeclareType( SPIRVPointer(tbuffers[tb].imageSampledTypeID, spv::StorageClassUniformConstant)); tbuffers[tb].variableID = editor.MakeId(); editor.AddVariable(rdcspv::Operation( spv::OpVariable, {arrayPtrType.value(), tbuffers[tb].variableID.value(), spv::StorageClassUniformConstant})); editor.SetName(tbuffers[tb].variableID, name); editor.AddDecoration(rdcspv::Operation( spv::OpDecorate, {tbuffers[tb].variableID.value(), (uint32_t)spv::DecorationDescriptorSet, 0})); editor.AddDecoration(rdcspv::Operation( spv::OpDecorate, {tbuffers[tb].variableID.value(), (uint32_t)spv::DecorationBinding, (uint32_t)tb})); } rdcspv::Id uint32Vec4ID; rdcspv::Id idxImageTypeID; rdcspv::Id idxImagePtr; rdcspv::Id idxSampledTypeID; if(draw->flags & DrawFlags::Indexed) { uint32Vec4ID = editor.DeclareType(SPIRVVector(scalar(), 4)); idxImageTypeID = editor.DeclareType( SPIRVImage(scalar(), spv::DimBuffer, 0, 0, 0, 1, spv::ImageFormatUnknown)); idxSampledTypeID = editor.DeclareType(SPIRVSampledImage(idxImageTypeID)); rdcspv::Id idxImagePtrType = editor.DeclareType(SPIRVPointer(idxSampledTypeID, spv::StorageClassUniformConstant)); idxImagePtr = editor.MakeId(); editor.AddVariable(rdcspv::Operation( spv::OpVariable, {idxImagePtrType.value(), idxImagePtr.value(), spv::StorageClassUniformConstant})); editor.SetName(idxImagePtr, "ibuffer"); editor.AddDecoration(rdcspv::Operation( spv::OpDecorate, {idxImagePtr.value(), (uint32_t)spv::DecorationDescriptorSet, 0})); editor.AddDecoration(rdcspv::Operation( spv::OpDecorate, {idxImagePtr.value(), (uint32_t)spv::DecorationBinding, 1})); } if(numInputs > 0) { editor.AddCapability(spv::CapabilitySampledBuffer); } rdcspv::Id outBufferVarID; rdcspv::Id numVertsConstID = editor.AddConstantImmediate(numVerts); rdcspv::Id numInstConstID = editor.AddConstantImmediate(draw->numInstances); rdcspv::Id numViewsConstID = editor.AddConstantImmediate(numViews); editor.SetName(numVertsConstID, "numVerts"); editor.SetName(numInstConstID, "numInsts"); editor.SetName(numViewsConstID, "numViews"); // declare the output buffer and its type { std::vector words; for(uint32_t o = 0; o < numOutputs; o++) words.push_back(outs[o].basetypeID); // struct vertex { ... outputs }; rdcspv::Id vertStructID = editor.DeclareStructType(words); editor.SetName(vertStructID, "vertex_struct"); // vertex vertArray[]; rdcspv::Id runtimeArrayID = editor.AddType(rdcspv::Operation( spv::OpTypeRuntimeArray, {editor.MakeId().value(), vertStructID.value()})); editor.SetName(runtimeArrayID, "vertex_array"); // struct meshOutput { vertex vertArray[]; }; rdcspv::Id outputStructID = editor.DeclareStructType({runtimeArrayID}); editor.SetName(outputStructID, "meshOutput"); // meshOutput * rdcspv::Id outputStructPtrID = editor.DeclareType(SPIRVPointer(outputStructID, spv::StorageClassUniform)); editor.SetName(outputStructPtrID, "meshOutput_ptr"); // meshOutput *outputData; outBufferVarID = editor.AddVariable(rdcspv::Operation( spv::OpVariable, {outputStructPtrID.value(), editor.MakeId().value(), spv::StorageClassUniform})); editor.SetName(outBufferVarID, "outputData"); uint32_t memberOffset = 0; for(uint32_t o = 0; o < numOutputs; o++) { uint32_t elemSize = 0; if(refl.outputSignature[o].compType == CompType::Double) elemSize = 8; else if(refl.outputSignature[o].compType == CompType::SInt || refl.outputSignature[o].compType == CompType::UInt || refl.outputSignature[o].compType == CompType::Float) elemSize = 4; else RDCERR("Unexpected component type for output signature element"); uint32_t numComps = refl.outputSignature[o].compCount; // ensure member is std430 packed (vec4 alignment for vec3/vec4) if(numComps == 2) memberOffset = AlignUp(memberOffset, 2U * elemSize); else if(numComps > 2) memberOffset = AlignUp(memberOffset, 4U * elemSize); // apply decoration to each member in the struct with its offset in the struct editor.AddDecoration(rdcspv::Operation( spv::OpMemberDecorate, {vertStructID.value(), o, spv::DecorationOffset, memberOffset})); memberOffset += elemSize * refl.outputSignature[o].compCount; } // align to 16 bytes (vec4) since we will almost certainly have // a vec4 in the struct somewhere, and even in std430 alignment, // the base struct alignment is still the largest base alignment // of any member bufStride = AlignUp16(memberOffset); // the array is the only element in the output struct, so // it's at offset 0 editor.AddDecoration(rdcspv::Operation(spv::OpMemberDecorate, {outputStructID.value(), 0, spv::DecorationOffset, 0})); // set array stride editor.AddDecoration(rdcspv::Operation( spv::OpDecorate, {runtimeArrayID.value(), spv::DecorationArrayStride, bufStride})); // set object type editor.AddDecoration( rdcspv::Operation(spv::OpDecorate, {outputStructID.value(), spv::DecorationBufferBlock})); // set binding editor.AddDecoration(rdcspv::Operation( spv::OpDecorate, {outBufferVarID.value(), spv::DecorationDescriptorSet, 0})); editor.AddDecoration( rdcspv::Operation(spv::OpDecorate, {outBufferVarID.value(), spv::DecorationBinding, 0})); } rdcspv::Id uint32Vec3ID = editor.DeclareType(SPIRVVector(scalar(), 3)); rdcspv::Id invocationPtr = editor.DeclareType(SPIRVPointer(uint32Vec3ID, spv::StorageClassInput)); rdcspv::Id invocationId = editor.AddVariable(rdcspv::Operation( spv::OpVariable, {invocationPtr.value(), editor.MakeId().value(), spv::StorageClassInput})); editor.AddDecoration(rdcspv::Operation( spv::OpDecorate, {invocationId.value(), spv::DecorationBuiltIn, spv::BuiltInGlobalInvocationId})); editor.SetName(invocationId, "rdoc_invocation"); // make a new entry point that will call the old function, then when it returns extract & write // the outputs. rdcspv::Id wrapperEntry = editor.MakeId(); // don't set a debug name, as some drivers get confused when this doesn't match the entry point // name :(. // editor.SetName(wrapperEntry, "RenderDoc_MeshFetch_Wrapper_Entrypoint"); // we remove all entry points and just create one of our own. rdcspv::Iter it = editor.Begin(SPIRVSection::EntryPoints); { // there should already have been at least one entry point RDCASSERT(it.opcode() == spv::OpEntryPoint); // and it should have been at least 5 words (if not more) since a vertex shader cannot function // without at least one interface ID. We only need one, so there should be plenty space. RDCASSERT(it.size() >= 5); editor.PreModify(it); it.nopRemove(5); it.word(1) = spv::ExecutionModelGLCompute; it.word(2) = wrapperEntry.value(); it.word(3) = MAKE_FOURCC('r', 'd', 'c', 0); it.word(4) = invocationId.value(); editor.PostModify(it); ++it; } for(rdcspv::Iter end = editor.End(SPIRVSection::EntryPoints); it < end; ++it) editor.Remove(it); // Strip away any execution modes from the original shaders for(it = editor.Begin(SPIRVSection::ExecutionMode); it < editor.End(SPIRVSection::ExecutionMode); ++it) { if(it.opcode() == spv::OpExecutionMode) { rdcspv::Id modeEntryID = rdcspv::Id::fromWord(it.word(1)); // We only need to be cautious about what we are stripping for the entry // that we are actually translating, the rest aren't used anyways. if(modeEntryID == entryID) { // Lets check to make sure we don't blindly strip away execution modes that // might actually have an impact on the behaviour of the shader. spv::ExecutionMode execMode = spv::ExecutionMode(it.word(2)); switch(execMode) { case spv::ExecutionModeXfb: break; default: RDCERR("Unexpected execution mode"); } } editor.Remove(it); } } // Add our compute shader execution mode editor.AddExecutionMode(wrapperEntry, spv::ExecutionModeLocalSize, {MeshOutputDispatchWidth, 1, 1}); rdcspv::Id uint32ID = editor.DeclareType(scalar()); // add the wrapper function { std::vector ops; rdcspv::Id voidType = editor.DeclareType(scalar()); rdcspv::Id funcType = editor.DeclareType(SPIRVFunction(voidType, {})); ops.push_back(rdcspv::Operation( spv::OpFunction, {voidType.value(), wrapperEntry.value(), spv::FunctionControlMaskNone, funcType.value()})); ops.push_back(rdcspv::Operation(spv::OpLabel, {editor.MakeId().value()})); { // uint3 invocationVec = gl_GlobalInvocationID; rdcspv::Id invocationVector = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpLoad, {uint32Vec3ID.value(), invocationVector.value(), invocationId.value()})); // uint invocation = invocationVec.x rdcspv::Id uintInvocationID = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpCompositeExtract, {uint32ID.value(), uintInvocationID.value(), invocationVector.value(), 0U})); // arraySlotID = uintInvocationID; rdcspv::Id arraySlotID = uintInvocationID; editor.SetName(uintInvocationID, "arraySlot"); // uint viewinst = uintInvocationID / numVerts rdcspv::Id viewinstID = editor.MakeId(); ops.push_back( rdcspv::Operation(spv::OpUDiv, {uint32ID.value(), viewinstID.value(), uintInvocationID.value(), numVertsConstID.value()})); editor.SetName(viewinstID, "viewInstance"); rdcspv::Id instID = editor.MakeId(); ops.push_back(rdcspv::Operation(spv::OpUMod, {uint32ID.value(), instID.value(), viewinstID.value(), numInstConstID.value()})); editor.SetName(instID, "instanceID"); rdcspv::Id viewID = editor.MakeId(); ops.push_back(rdcspv::Operation(spv::OpUDiv, {uint32ID.value(), viewID.value(), viewinstID.value(), numInstConstID.value()})); editor.SetName(viewID, "viewID"); // bool inBounds = viewID < numViews; rdcspv::Id inBounds = editor.MakeId(); ops.push_back(rdcspv::Operation(spv::OpULessThan, {editor.DeclareType(scalar()).value(), inBounds.value(), viewID.value(), numViewsConstID.value()})); // if(inBounds) goto continueLabel; else goto killLabel; rdcspv::Id killLabel = editor.MakeId(); rdcspv::Id continueLabel = editor.MakeId(); ops.push_back(rdcspv::Operation(spv::OpSelectionMerge, {killLabel.value(), spv::SelectionControlMaskNone})); ops.push_back(rdcspv::Operation( spv::OpBranchConditional, {inBounds.value(), continueLabel.value(), killLabel.value()})); // continueLabel: ops.push_back(rdcspv::Operation(spv::OpLabel, {continueLabel.value()})); // uint vtx = uintInvocationID % numVerts rdcspv::Id vtxID = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpUMod, {uint32ID.value(), vtxID.value(), uintInvocationID.value(), numVertsConstID.value()})); editor.SetName(vtxID, "vertexID"); rdcspv::Id vertexIndexID = vtxID; // if we're indexing, look up the index buffer. We don't have to apply vertexOffset - it was // already applied when we read back and uniq-ified the index buffer. if(draw->flags & DrawFlags::Indexed) { // sampledimage idximg = *idximgPtr; rdcspv::Id loaded = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpLoad, {idxSampledTypeID.value(), loaded.value(), idxImagePtr.value()})); // image rawimg = imageFromSampled(idximg); rdcspv::Id rawimg = editor.MakeId(); ops.push_back(rdcspv::Operation(spv::OpImage, {idxImageTypeID.value(), rawimg.value(), loaded.value()})); // uvec4 result = texelFetch(rawimg, vtxID); rdcspv::Id result = editor.MakeId(); ops.push_back(rdcspv::Operation(spv::OpImageFetch, {uint32Vec4ID.value(), result.value(), rawimg.value(), vertexIndexID.value()})); // vertexIndex = result.x; vertexIndexID = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpCompositeExtract, {uint32ID.value(), vertexIndexID.value(), result.value(), 0})); } // we use the current value of vertexIndex and use instID, to lookup per-vertex and // per-instance attributes. This is because when we fetched the vertex data, we advanced by // (in non-indexed draws) vertexOffset, and by instanceOffset. Rather than fetching data // that's only used as padding skipped over by these offsets. rdcspv::Id vertexLookupID = vertexIndexID; rdcspv::Id instanceLookupID = instID; if(!(draw->flags & DrawFlags::Indexed)) { // for non-indexed draws, we manually apply the vertex offset, but here after we used the // 0-based one to calculate the array slot vertexIndexID = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpIAdd, {uint32ID.value(), vertexIndexID.value(), vtxID.value(), editor.AddConstantImmediate(draw->vertexOffset).value()})); } editor.SetName(vertexIndexID, "vertexIndex"); // instIndex = inst + instOffset rdcspv::Id instIndexID = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpIAdd, {uint32ID.value(), instIndexID.value(), instID.value(), editor.AddConstantImmediate(draw->instanceOffset).value()})); editor.SetName(instIndexID, "instanceIndex"); rdcspv::Id idxs[64] = {}; for(size_t i = 0; i < refl.inputSignature.size(); i++) { ShaderBuiltin builtin = refl.inputSignature[i].systemValue; if(builtin != ShaderBuiltin::Undefined) { rdcspv::Id valueID; CompType compType = CompType::UInt; if(builtin == ShaderBuiltin::VertexIndex) { valueID = vertexIndexID; } else if(builtin == ShaderBuiltin::InstanceIndex) { valueID = instIndexID; } else if(builtin == ShaderBuiltin::ViewportIndex) { valueID = viewID; } else if(builtin == ShaderBuiltin::BaseVertex) { if(draw->flags & DrawFlags::Indexed) { valueID = editor.AddConstantImmediate(draw->vertexOffset); } else { valueID = editor.AddConstantImmediate(draw->baseVertex); compType = CompType::SInt; } } else if(builtin == ShaderBuiltin::BaseInstance) { valueID = editor.AddConstantImmediate(draw->instanceOffset); } else if(builtin == ShaderBuiltin::DrawIndex) { valueID = editor.AddConstantImmediate(draw->drawIndex); } if(valueID) { if(refl.inputSignature[i].compType == compType) { ops.push_back( rdcspv::Operation(spv::OpStore, {ins[i].variableID.value(), valueID.value()})); } else { rdcspv::Id castedValue = editor.MakeId(); // assume we can just bitcast ops.push_back(rdcspv::Operation( spv::OpBitcast, {ins[i].basetypeID.value(), castedValue.value(), valueID.value()})); ops.push_back(rdcspv::Operation(spv::OpStore, {ins[i].variableID.value(), castedValue.value()})); } } else { RDCERR("Unsupported/unsupported built-in input %s", ToStr(builtin).c_str()); } } else { if(idxs[i] == 0) idxs[i] = editor.AddConstantImmediate((uint32_t)i); if(idxs[refl.inputSignature[i].regIndex] == 0) idxs[refl.inputSignature[i].regIndex] = editor.AddConstantImmediate(refl.inputSignature[i].regIndex); tbufferIDs tb = tbuffers[ins[i].tbuffer]; uint32_t location = refl.inputSignature[i].regIndex; rdcspv::Id ptrId = editor.MakeId(); // sampledimage *imgPtr = xxx_tbuffers[i]; ops.push_back(rdcspv::Operation( spv::OpAccessChain, {tb.pointerTypeID.value(), ptrId.value(), tb.variableID.value(), idxs[refl.inputSignature[i].regIndex].value()})); // sampledimage img = *imgPtr; rdcspv::Id loaded = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpLoad, {tb.imageSampledTypeID.value(), loaded.value(), ptrId.value()})); // image rawimg = imageFromSampled(img); rdcspv::Id rawimg = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpImage, {tb.imageTypeID.value(), rawimg.value(), loaded.value()})); // vec4 result = texelFetch(rawimg, vtxID or instID); rdcspv::Id idx = vertexLookupID; if(location < instDivisor.size()) { uint32_t divisor = instDivisor[location]; if(divisor == ~0U) { // this magic value indicates vertex-rate data idx = vertexLookupID; } else if(divisor == 0) { // if the divisor is 0, all instances read the first value. idx = editor.AddConstantImmediate(0); } else if(divisor == 1) { // if the divisor is 1, it's just regular instancing idx = instanceLookupID; } else { // otherwise we divide by the divisor idx = editor.MakeId(); rdcspv::Id divisorId = editor.AddConstantImmediate(divisor); ops.push_back(rdcspv::Operation( spv::OpUDiv, {uint32ID.value(), idx.value(), instanceLookupID.value(), divisorId.value()})); } } if(refl.inputSignature[i].compType == CompType::Double) { // since doubles are packed into two uints, we need to multiply the index by two rdcspv::Id doubled = editor.MakeId(); ops.push_back( rdcspv::Operation(spv::OpIMul, {uint32ID.value(), doubled.value(), idx.value(), editor.AddConstantImmediate(2).value()})); idx = doubled; } rdcspv::Id result = editor.MakeId(); ops.push_back(rdcspv::Operation(spv::OpImageFetch, {ins[i].vec4ID.value(), result.value(), rawimg.value(), idx.value()})); if(refl.inputSignature[i].compType == CompType::Double) { // since doubles are packed into two uints, we now need to fetch more data and do // packing. We can fetch the data unconditionally since it's harmless to read out of the // bounds of the buffer rdcspv::Id nextidx = editor.MakeId(); ops.push_back( rdcspv::Operation(spv::OpIAdd, {uint32ID.value(), nextidx.value(), idx.value(), editor.AddConstantImmediate(1).value()})); rdcspv::Id result2 = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpImageFetch, {ins[i].vec4ID.value(), result2.value(), rawimg.value(), nextidx.value()})); rdcspv::Id glsl450 = editor.ImportExtInst("GLSL.std.450"); rdcspv::Id uvec2Type = editor.DeclareType(SPIRVVector(scalar(), 2)); rdcspv::Id comps[4] = {}; for(uint32_t c = 0; c < refl.inputSignature[i].compCount; c++) { // first extract the uvec2 we want rdcspv::Id packed = editor.MakeId(); // uvec2 packed = result.[xy/zw] / result2.[xy/zw]; ops.push_back(rdcspv::Operation(spv::OpVectorShuffle, {uvec2Type.value(), packed.value(), result.value(), result2.value(), c * 2 + 0, c * 2 + 1})); char swizzle[] = "xyzw"; editor.SetName(packed, StringFormat::Fmt("packed_%c", swizzle[c]).c_str()); // double comp = PackDouble2x32(packed); comps[c] = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpExtInst, { editor.DeclareType(scalar()).value(), comps[c].value(), glsl450.value(), GLSLstd450PackDouble2x32, packed.value(), })); } // if there's only one component it's ready, otherwise construct a vector if(refl.inputSignature[i].compCount == 1) { result = comps[0]; } else { result = editor.MakeId(); std::vector words = {ins[i].basetypeID.value(), result.value()}; for(uint32_t c = 0; c < refl.inputSignature[i].compCount; c++) words.push_back(comps[c].value()); // baseTypeN value = result.xyz; ops.push_back(rdcspv::Operation(spv::OpCompositeConstruct, words)); } } else if(refl.inputSignature[i].compCount == 1) { // for one component, extract x rdcspv::Id swizzleIn = result; result = editor.MakeId(); // baseType value = result.x; ops.push_back(rdcspv::Operation( spv::OpCompositeExtract, {ins[i].basetypeID.value(), result.value(), swizzleIn.value(), 0})); } else if(refl.inputSignature[i].compCount != 4) { // for less than 4 components, extract the sub-vector rdcspv::Id swizzleIn = result; result = editor.MakeId(); std::vector words = {ins[i].basetypeID.value(), result.value(), swizzleIn.value(), swizzleIn.value()}; for(uint32_t c = 0; c < refl.inputSignature[i].compCount; c++) words.push_back(c); // baseTypeN value = result.xyz; ops.push_back(rdcspv::Operation(spv::OpVectorShuffle, words)); } // copy the 4 component result directly // not a composite type, we can store directly if(patchData.inputs[i].accessChain.empty()) { // *global = value ops.push_back( rdcspv::Operation(spv::OpStore, {ins[i].variableID.value(), result.value()})); } else { // for composite types we need to access chain first rdcspv::Id subElement = editor.MakeId(); std::vector words = {ins[i].privatePtrID.value(), subElement.value(), patchData.inputs[i].ID}; for(uint32_t accessIdx : patchData.inputs[i].accessChain) { if(idxs[accessIdx] == 0) idxs[accessIdx] = editor.AddConstantImmediate(accessIdx); words.push_back(idxs[accessIdx].value()); } ops.push_back(rdcspv::Operation(spv::OpAccessChain, words)); ops.push_back(rdcspv::Operation(spv::OpStore, {subElement.value(), result.value()})); } } } // real_main(); ops.push_back(rdcspv::Operation( spv::OpFunctionCall, {voidType.value(), editor.MakeId().value(), entryID.value()})); rdcspv::Id zero = editor.AddConstantImmediate(0); for(uint32_t o = 0; o < numOutputs; o++) { rdcspv::Id loaded; // not a structure member or array child, can load directly if(patchData.outputs[o].accessChain.empty()) { loaded = editor.MakeId(); // type loaded = *globalvar; ops.push_back(rdcspv::Operation( spv::OpLoad, {outs[o].basetypeID.value(), loaded.value(), patchData.outputs[o].ID})); } else { rdcspv::Id readPtr = editor.MakeId(); loaded = editor.MakeId(); // structure member, need to access chain first std::vector words = {outs[o].privatePtrID.value(), readPtr.value(), patchData.outputs[o].ID}; for(uint32_t idx : patchData.outputs[o].accessChain) { if(idxs[idx] == 0) idxs[idx] = editor.AddConstantImmediate(idx); words.push_back(idxs[idx].value()); } // type *readPtr = globalvar.globalsub...; ops.push_back(rdcspv::Operation(spv::OpAccessChain, words)); // type loaded = *readPtr; ops.push_back(rdcspv::Operation( spv::OpLoad, {outs[o].basetypeID.value(), loaded.value(), readPtr.value()})); } // access chain the destination // type *writePtr = outBuffer.verts[arraySlot].outputN rdcspv::Id writePtr = editor.MakeId(); ops.push_back(rdcspv::Operation( spv::OpAccessChain, {outs[o].uniformPtrID.value(), writePtr.value(), outBufferVarID.value(), zero.value(), arraySlotID.value(), outs[o].constID.value()})); // *writePtr = loaded; ops.push_back(rdcspv::Operation(spv::OpStore, {writePtr.value(), loaded.value()})); } // goto killLabel; ops.push_back(rdcspv::Operation(spv::OpBranch, {killLabel.value()})); // killLabel: ops.push_back(rdcspv::Operation(spv::OpLabel, {killLabel.value()})); } ops.push_back(rdcspv::Operation(spv::OpReturn, {})); ops.push_back(rdcspv::Operation(spv::OpFunctionEnd, {})); editor.AddFunction(ops.data(), ops.size()); } } void VulkanReplay::ClearPostVSCache() { VkDevice dev = m_Device; for(auto it = m_PostVS.Data.begin(); it != m_PostVS.Data.end(); ++it) { if(it->second.vsout.idxbuf != VK_NULL_HANDLE) { m_pDriver->vkDestroyBuffer(dev, it->second.vsout.idxbuf, NULL); m_pDriver->vkFreeMemory(dev, it->second.vsout.idxbufmem, NULL); } m_pDriver->vkDestroyBuffer(dev, it->second.vsout.buf, NULL); m_pDriver->vkFreeMemory(dev, it->second.vsout.bufmem, NULL); if(it->second.gsout.buf != VK_NULL_HANDLE) { m_pDriver->vkDestroyBuffer(dev, it->second.gsout.buf, NULL); m_pDriver->vkFreeMemory(dev, it->second.gsout.bufmem, NULL); } } m_PostVS.Data.clear(); } void VulkanReplay::PatchReservedDescriptors(const VulkanStatePipeline &pipe, VkDescriptorPool &descpool, std::vector &setLayouts, std::vector &descSets, VkShaderStageFlagBits patchedBindingStage, const VkDescriptorSetLayoutBinding *newBindings, size_t newBindingsCount) { VkDevice dev = m_Device; VulkanCreationInfo &creationInfo = m_pDriver->m_CreationInfo; const VulkanCreationInfo::Pipeline &pipeInfo = creationInfo.m_Pipeline[pipe.pipeline]; VkResult vkr = VK_SUCCESS; { std::vector descWrites; std::vector allocImgWrites; std::vector allocBufWrites; std::vector allocBufViewWrites; // one for each descriptor type. 1 of each to start with, we then increment for each descriptor // we need to allocate VkDescriptorPoolSize poolSizes[11] = { {VK_DESCRIPTOR_TYPE_SAMPLER, 1}, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1}, {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1}, {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1}, {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1}, }; // count up our own for(size_t i = 0; i < newBindingsCount; i++) poolSizes[newBindings[i].descriptorType].descriptorCount += newBindings[i].descriptorCount; const std::vector &descSetLayoutIds = creationInfo.m_PipelineLayout[pipeInfo.layout].descSetLayouts; // need to add our added bindings to the first descriptor set std::vector bindings(newBindings, newBindings + newBindingsCount); // if there are fewer sets bound than were declared in the pipeline layout, only process the // bound sets (as otherwise we'd fail to copy from them). Assume the application knew what it // was doing and the other sets are statically unused. setLayouts.resize(RDCMIN(pipe.descSets.size(), descSetLayoutIds.size())); // need at least one set, if the shader isn't using any we'll just make our own if(setLayouts.empty()) setLayouts.resize(1); for(size_t i = 0; i < setLayouts.size(); i++) { bool hasImmutableSamplers = false; // except for the first layout we need to start from scratch if(i > 0) bindings.clear(); // if the shader had no descriptor sets at all, i will be invalid, so just skip and add a set // with only our own bindings. if(i < descSetLayoutIds.size()) { const DescSetLayout &origLayout = creationInfo.m_DescSetLayout[descSetLayoutIds[i]]; for(size_t b = 0; b < origLayout.bindings.size(); b++) { const DescSetLayout::Binding &bind = origLayout.bindings[b]; // skip empty bindings if(bind.descriptorCount == 0 || bind.stageFlags == 0) continue; // make room in the pool poolSizes[bind.descriptorType].descriptorCount += bind.descriptorCount; VkDescriptorSetLayoutBinding newBind; // offset the binding. We offset all sets to make it easier for patching - don't need to // conditionally patch shader bindings depending on which set they're in. newBind.binding = uint32_t(b + newBindingsCount); newBind.descriptorCount = bind.descriptorCount; newBind.descriptorType = bind.descriptorType; // we only need it available for compute, just make all bindings visible otherwise dynamic // buffer offsets could be indexed wrongly. Consider the case where we have binding 0 as a // fragment UBO, and binding 1 as a vertex UBO. Then there are two dynamic offsets, and // the second is the one we want to use with ours. If we only add the compute visibility // bit to the second UBO, then suddenly it's the *first* offset that we must provide. // Instead of trying to remap offsets to match, we simply make every binding compute // visible so the ordering is still the same. Since compute and graphics are disjoint this // is safe. if(patchedBindingStage) newBind.stageFlags = patchedBindingStage; else newBind.stageFlags = bind.stageFlags; if(bind.immutableSampler) { hasImmutableSamplers = true; VkSampler *samplers = new VkSampler[bind.descriptorCount]; newBind.pImmutableSamplers = samplers; for(uint32_t s = 0; s < bind.descriptorCount; s++) samplers[s] = GetResourceManager()->GetCurrentHandle(bind.immutableSampler[s]); } else { newBind.pImmutableSamplers = NULL; } bindings.push_back(newBind); } } VkDescriptorSetLayoutCreateInfo descsetLayoutInfo = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, NULL, 0, (uint32_t)bindings.size(), bindings.data(), }; // create new offseted descriptor layout vkr = m_pDriver->vkCreateDescriptorSetLayout(dev, &descsetLayoutInfo, NULL, &setLayouts[i]); RDCASSERTEQUAL(vkr, VK_SUCCESS); if(hasImmutableSamplers) { for(const VkDescriptorSetLayoutBinding &bind : bindings) delete[] bind.pImmutableSamplers; } } VkDescriptorPoolCreateInfo poolCreateInfo = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; // 1 set for each layout poolCreateInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; poolCreateInfo.maxSets = (uint32_t)setLayouts.size(); poolCreateInfo.poolSizeCount = ARRAY_COUNT(poolSizes); poolCreateInfo.pPoolSizes = poolSizes; // create descriptor pool with enough space for our descriptors vkr = m_pDriver->vkCreateDescriptorPool(dev, &poolCreateInfo, NULL, &descpool); RDCASSERTEQUAL(vkr, VK_SUCCESS); // allocate all the descriptors VkDescriptorSetAllocateInfo descSetAllocInfo = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, NULL, descpool, (uint32_t)setLayouts.size(), setLayouts.data(), }; descSets.resize(setLayouts.size()); m_pDriver->vkAllocateDescriptorSets(dev, &descSetAllocInfo, descSets.data()); // copy the data across from the real descriptors into our adjusted bindings for(size_t i = 0; i < descSetLayoutIds.size(); i++) { const DescSetLayout &origLayout = creationInfo.m_DescSetLayout[descSetLayoutIds[i]]; if(i >= pipe.descSets.size()) continue; if(pipe.descSets[i].descSet == ResourceId()) continue; WrappedVulkan::DescriptorSetInfo &setInfo = m_pDriver->m_DescriptorSetState[pipe.descSets[i].descSet]; { // push descriptors don't have a source to copy from, we need to add writes VkWriteDescriptorSet write = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; write.dstSet = descSets[i]; for(size_t b = 0; b < origLayout.bindings.size(); b++) { const DescSetLayout::Binding &bind = origLayout.bindings[b]; // skip empty bindings if(bind.descriptorCount == 0 || bind.stageFlags == 0) continue; DescriptorSetBindingElement *slot = setInfo.currentBindings[b]; write.dstBinding = uint32_t(b + newBindingsCount); write.dstArrayElement = 0; write.descriptorCount = bind.descriptorCount; write.descriptorType = bind.descriptorType; switch(write.descriptorType) { case VK_DESCRIPTOR_TYPE_SAMPLER: case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: { VkDescriptorImageInfo *out = new VkDescriptorImageInfo[write.descriptorCount]; for(uint32_t w = 0; w < write.descriptorCount; w++) out[w] = slot[w].imageInfo; write.pImageInfo = out; allocImgWrites.push_back(out); break; } case VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER: case VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER: { VkBufferView *out = new VkBufferView[write.descriptorCount]; for(uint32_t w = 0; w < write.descriptorCount; w++) out[w] = slot[w].texelBufferView; write.pTexelBufferView = out; allocBufViewWrites.push_back(out); break; } case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER: case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC: case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC: { VkDescriptorBufferInfo *out = new VkDescriptorBufferInfo[write.descriptorCount]; for(uint32_t w = 0; w < write.descriptorCount; w++) out[w] = slot[w].bufferInfo; write.pBufferInfo = out; allocBufWrites.push_back(out); break; } default: RDCERR("Unexpected descriptor type %d", write.descriptorType); } // start with no descriptors write.descriptorCount = 0; for(uint32_t w = 0; w < bind.descriptorCount; w++) { // if this write is valid, we increment the descriptor count and continue if(IsValid(write, w - write.dstArrayElement)) { write.descriptorCount++; } else { // if this write isn't valid, then we first check to see if we had any previous // pending writes in the array we were going to batch together, if so we add them. if(write.descriptorCount > 0) descWrites.push_back(write); // skip past any previous descriptors we just wrote, as well as the current invalid // one if(write.pBufferInfo) write.pBufferInfo += write.descriptorCount + 1; if(write.pImageInfo) write.pImageInfo += write.descriptorCount + 1; if(write.pTexelBufferView) write.pTexelBufferView += write.descriptorCount + 1; // now start again from 0 descriptors, at the next array element write.dstArrayElement += write.descriptorCount + 1; write.descriptorCount = 0; } } // if there are any left, add them here if(write.descriptorCount > 0) descWrites.push_back(write); // don't leak the arrays and cause double deletes, NULL them after each time write.pImageInfo = NULL; write.pBufferInfo = NULL; write.pTexelBufferView = NULL; } } } m_pDriver->vkUpdateDescriptorSets(dev, (uint32_t)descWrites.size(), descWrites.data(), 0, NULL); // delete allocated arrays for descriptor writes for(VkDescriptorBufferInfo *a : allocBufWrites) delete[] a; for(VkDescriptorImageInfo *a : allocImgWrites) delete[] a; for(VkBufferView *a : allocBufViewWrites) delete[] a; } } void VulkanReplay::FetchVSOut(uint32_t eventId) { const VulkanRenderState &state = m_pDriver->m_RenderState; VulkanCreationInfo &creationInfo = m_pDriver->m_CreationInfo; const VulkanCreationInfo::Pipeline &pipeInfo = creationInfo.m_Pipeline[state.graphics.pipeline]; const DrawcallDescription *drawcall = m_pDriver->GetDrawcall(eventId); const VulkanCreationInfo::ShaderModule &moduleInfo = creationInfo.m_ShaderModule[pipeInfo.shaders[0].module]; ShaderReflection *refl = pipeInfo.shaders[0].refl; // set defaults so that we don't try to fetch this output again if something goes wrong and the // same event is selected again { m_PostVS.Data[eventId].vsin.topo = pipeInfo.topology; m_PostVS.Data[eventId].vsout.buf = VK_NULL_HANDLE; m_PostVS.Data[eventId].vsout.bufmem = VK_NULL_HANDLE; m_PostVS.Data[eventId].vsout.instStride = 0; m_PostVS.Data[eventId].vsout.vertStride = 0; m_PostVS.Data[eventId].vsout.numViews = 1; m_PostVS.Data[eventId].vsout.nearPlane = 0.0f; m_PostVS.Data[eventId].vsout.farPlane = 0.0f; m_PostVS.Data[eventId].vsout.useIndices = false; m_PostVS.Data[eventId].vsout.hasPosOut = false; m_PostVS.Data[eventId].vsout.idxbuf = VK_NULL_HANDLE; m_PostVS.Data[eventId].vsout.idxbufmem = VK_NULL_HANDLE; m_PostVS.Data[eventId].vsout.topo = pipeInfo.topology; } // no outputs from this shader? unexpected but theoretically possible (dummy VS before // tessellation maybe). Just fill out an empty data set if(refl->outputSignature.empty()) return; // we go through the driver for all these creations since they need to be properly // registered in order to be put in the partial replay state VkResult vkr = VK_SUCCESS; VkDevice dev = m_Device; VkDescriptorPool descpool; std::vector setLayouts; std::vector descSets; VkPipelineLayout pipeLayout; VkGraphicsPipelineCreateInfo pipeCreateInfo; // get pipeline create info m_pDriver->GetShaderCache()->MakeGraphicsPipelineInfo(pipeCreateInfo, state.graphics.pipeline); VkDescriptorSetLayoutBinding newBindings[] = { // output buffer { 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL, }, // index buffer (if needed) { 1, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL, }, // vertex buffers (float type) { 2, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, MeshOutputTBufferArraySize, VK_SHADER_STAGE_COMPUTE_BIT, NULL, }, // vertex buffers (uint32_t type) { 3, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, MeshOutputTBufferArraySize, VK_SHADER_STAGE_COMPUTE_BIT, NULL, }, // vertex buffers (int32_t type) { 4, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, MeshOutputTBufferArraySize, VK_SHADER_STAGE_COMPUTE_BIT, NULL, }, }; RDCCOMPILE_ASSERT(ARRAY_COUNT(newBindings) == MeshOutputReservedBindings, "MeshOutputReservedBindings is wrong"); // create a duplicate set of descriptor sets, all visible to compute, with bindings shifted to // account for new ones we need. This also copies the existing bindings into the new sets PatchReservedDescriptors(m_pDriver->m_RenderState.graphics, descpool, setLayouts, descSets, VK_SHADER_STAGE_COMPUTE_BIT, newBindings, ARRAY_COUNT(newBindings)); // create pipeline layout with new descriptor set layouts { std::vector push = creationInfo.m_PipelineLayout[pipeInfo.layout].pushRanges; // ensure the push range is visible to the compute shader for(VkPushConstantRange &range : push) range.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; VkPipelineLayoutCreateInfo pipeLayoutInfo = { VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, NULL, 0, (uint32_t)setLayouts.size(), setLayouts.data(), (uint32_t)push.size(), push.data(), }; vkr = m_pDriver->vkCreatePipelineLayout(dev, &pipeLayoutInfo, NULL, &pipeLayout); RDCASSERTEQUAL(vkr, VK_SUCCESS); } VkBuffer meshBuffer = VK_NULL_HANDLE, readbackBuffer = VK_NULL_HANDLE; VkDeviceMemory meshMem = VK_NULL_HANDLE, readbackMem = VK_NULL_HANDLE; VkBuffer uniqIdxBuf = VK_NULL_HANDLE; VkDeviceMemory uniqIdxBufMem = VK_NULL_HANDLE; VkBufferView uniqIdxBufView = VK_NULL_HANDLE; VkBuffer rebasedIdxBuf = VK_NULL_HANDLE; VkDeviceMemory rebasedIdxBufMem = VK_NULL_HANDLE; uint32_t numVerts = drawcall->numIndices; VkDeviceSize bufSize = 0; uint32_t numViews = 1; { const VulkanCreationInfo::RenderPass &rp = creationInfo.m_RenderPass[state.renderPass]; if(state.subpass < rp.subpasses.size()) { numViews = RDCMAX(numViews, (uint32_t)rp.subpasses[state.subpass].multiviews.size()); } else { RDCERR("Subpass is out of bounds to renderpass creation info"); } } uint32_t idxsize = state.ibuffer.bytewidth; uint32_t maxIndex = RDCMAX(drawcall->baseVertex, 0) + numVerts - 1; uint32_t maxInstance = drawcall->instanceOffset + drawcall->numInstances - 1; if(drawcall->flags & DrawFlags::Indexed) { bool index16 = (idxsize == 2); bytebuf idxdata; std::vector indices; uint16_t *idx16 = NULL; uint32_t *idx32 = NULL; // fetch ibuffer if(state.ibuffer.buf != ResourceId()) GetBufferData(state.ibuffer.buf, state.ibuffer.offs + drawcall->indexOffset * idxsize, uint64_t(drawcall->numIndices) * idxsize, idxdata); // figure out what the maximum index could be, so we can clamp our index buffer to something // sane uint32_t maxIdx = 0; // if there are no active bindings assume the vertex shader is generating its own data // and don't clamp the indices if(pipeCreateInfo.pVertexInputState->vertexBindingDescriptionCount == 0) maxIdx = ~0U; for(uint32_t b = 0; b < pipeCreateInfo.pVertexInputState->vertexBindingDescriptionCount; b++) { const VkVertexInputBindingDescription &input = pipeCreateInfo.pVertexInputState->pVertexBindingDescriptions[b]; // only vertex inputs (not instance inputs) count if(input.inputRate == VK_VERTEX_INPUT_RATE_VERTEX) { if(b >= state.vbuffers.size()) continue; ResourceId buf = state.vbuffers[b].buf; VkDeviceSize offs = state.vbuffers[b].offs; VkDeviceSize bufsize = creationInfo.m_Buffer[buf].size; // the maximum valid index on this particular input is the one that reaches // the end of the buffer. The maximum valid index at all is the one that reads // off the end of ALL buffers (so we max it with any other maxindex value // calculated). if(input.stride > 0) maxIdx = RDCMAX(maxIdx, uint32_t((bufsize - offs) / input.stride)); } } // in case the vertex buffers were set but had invalid stride (0), max with the number // of vertices too. This is fine since the max here is just a conservative limit maxIdx = RDCMAX(maxIdx, drawcall->numIndices); // do ibuffer rebasing/remapping idx16 = (uint16_t *)&idxdata[0]; idx32 = (uint32_t *)&idxdata[0]; // only read as many indices as were available in the buffer uint32_t numIndices = RDCMIN(uint32_t(index16 ? idxdata.size() / 2 : idxdata.size() / 4), drawcall->numIndices); uint32_t idxclamp = 0; if(drawcall->baseVertex < 0) idxclamp = uint32_t(-drawcall->baseVertex); // grab all unique vertex indices referenced for(uint32_t i = 0; i < numIndices; i++) { uint32_t i32 = index16 ? uint32_t(idx16[i]) : idx32[i]; // apply baseVertex but clamp to 0 (don't allow index to become negative) if(i32 < idxclamp) i32 = 0; else if(drawcall->baseVertex < 0) i32 -= idxclamp; else if(drawcall->baseVertex > 0) i32 += drawcall->baseVertex; // we clamp to maxIdx here, to avoid any invalid indices like 0xffffffff // from filtering through. Worst case we index to the end of the vertex // buffers which is generally much more reasonable i32 = RDCMIN(maxIdx, i32); auto it = std::lower_bound(indices.begin(), indices.end(), i32); if(it != indices.end() && *it == i32) continue; indices.insert(it, i32); } // if we read out of bounds, we'll also have a 0 index being referenced // (as 0 is read). Don't insert 0 if we already have 0 though if(numIndices < drawcall->numIndices && (indices.empty() || indices[0] != 0)) indices.insert(indices.begin(), 0); maxIndex = indices.back(); // set numVerts numVerts = (uint32_t)indices.size(); // An index buffer could be something like: 500, 501, 502, 501, 503, 502 // in which case we can't use the existing index buffer without filling 499 slots of vertex // data with padding. Instead we rebase the indices based on the smallest vertex so it becomes // 0, 1, 2, 1, 3, 2 and then that matches our stream-out'd buffer. // // Note that there could also be gaps, like: 500, 501, 502, 510, 511, 512 // which would become 0, 1, 2, 3, 4, 5 and so the old index buffer would no longer be valid. // We just stream-out a tightly packed list of unique indices, and then remap the index buffer // so that what did point to 500 points to 0 (accounting for rebasing), and what did point // to 510 now points to 3 (accounting for the unique sort). // we use a map here since the indices may be sparse. Especially considering if an index // is 'invalid' like 0xcccccccc then we don't want an array of 3.4 billion entries. std::map indexRemap; for(size_t i = 0; i < indices.size(); i++) { // by definition, this index will only appear once in indices[] indexRemap[indices[i]] = i; } // create buffer with unique 0-based indices VkBufferCreateInfo bufInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, NULL, 0, indices.size() * sizeof(uint32_t), VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, }; vkr = m_pDriver->vkCreateBuffer(dev, &bufInfo, NULL, &uniqIdxBuf); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkMemoryRequirements mrq = {0}; m_pDriver->vkGetBufferMemoryRequirements(dev, uniqIdxBuf, &mrq); VkMemoryAllocateInfo allocInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size, m_pDriver->GetUploadMemoryIndex(mrq.memoryTypeBits), }; vkr = m_pDriver->vkAllocateMemory(dev, &allocInfo, NULL, &uniqIdxBufMem); if(vkr == VK_ERROR_OUT_OF_DEVICE_MEMORY || vkr == VK_ERROR_OUT_OF_HOST_MEMORY) { RDCWARN("Failed to allocate %llu bytes for unique index buffer", mrq.size); return; } RDCASSERTEQUAL(vkr, VK_SUCCESS); vkr = m_pDriver->vkBindBufferMemory(dev, uniqIdxBuf, uniqIdxBufMem, 0); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkBufferViewCreateInfo viewInfo = { VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO, NULL, 0, uniqIdxBuf, VK_FORMAT_R32_UINT, 0, VK_WHOLE_SIZE, }; vkr = m_pDriver->vkCreateBufferView(dev, &viewInfo, NULL, &uniqIdxBufView); RDCASSERTEQUAL(vkr, VK_SUCCESS); byte *idxData = NULL; vkr = m_pDriver->vkMapMemory(m_Device, uniqIdxBufMem, 0, VK_WHOLE_SIZE, 0, (void **)&idxData); RDCASSERTEQUAL(vkr, VK_SUCCESS); memcpy(idxData, &indices[0], indices.size() * sizeof(uint32_t)); VkMappedMemoryRange range = { VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, NULL, uniqIdxBufMem, 0, VK_WHOLE_SIZE, }; vkr = m_pDriver->vkFlushMappedMemoryRanges(m_Device, 1, &range); RDCASSERTEQUAL(vkr, VK_SUCCESS); m_pDriver->vkUnmapMemory(m_Device, uniqIdxBufMem); // rebase existing index buffer to point to the right elements in our stream-out'd // vertex buffer for(uint32_t i = 0; i < numIndices; i++) { uint32_t i32 = index16 ? uint32_t(idx16[i]) : idx32[i]; // preserve primitive restart indices if(i32 == (index16 ? 0xffff : 0xffffffff)) continue; // apply baseVertex but clamp to 0 (don't allow index to become negative) if(i32 < idxclamp) i32 = 0; else if(drawcall->baseVertex < 0) i32 -= idxclamp; else if(drawcall->baseVertex > 0) i32 += drawcall->baseVertex; if(index16) idx16[i] = uint16_t(indexRemap[i32]); else idx32[i] = uint32_t(indexRemap[i32]); } bufInfo.size = RDCMAX((VkDeviceSize)64, (VkDeviceSize)idxdata.size()); bufInfo.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT; vkr = m_pDriver->vkCreateBuffer(dev, &bufInfo, NULL, &rebasedIdxBuf); RDCASSERTEQUAL(vkr, VK_SUCCESS); m_pDriver->vkGetBufferMemoryRequirements(dev, rebasedIdxBuf, &mrq); allocInfo.allocationSize = mrq.size; allocInfo.memoryTypeIndex = m_pDriver->GetUploadMemoryIndex(mrq.memoryTypeBits); vkr = m_pDriver->vkAllocateMemory(dev, &allocInfo, NULL, &rebasedIdxBufMem); if(vkr == VK_ERROR_OUT_OF_DEVICE_MEMORY || vkr == VK_ERROR_OUT_OF_HOST_MEMORY) { RDCWARN("Failed to allocate %llu bytes for rebased index buffer", mrq.size); return; } RDCASSERTEQUAL(vkr, VK_SUCCESS); vkr = m_pDriver->vkBindBufferMemory(dev, rebasedIdxBuf, rebasedIdxBufMem, 0); RDCASSERTEQUAL(vkr, VK_SUCCESS); vkr = m_pDriver->vkMapMemory(m_Device, rebasedIdxBufMem, 0, VK_WHOLE_SIZE, 0, (void **)&idxData); RDCASSERTEQUAL(vkr, VK_SUCCESS); memcpy(idxData, idxdata.data(), idxdata.size()); VkMappedMemoryRange rebasedRange = { VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, NULL, rebasedIdxBufMem, 0, VK_WHOLE_SIZE, }; vkr = m_pDriver->vkFlushMappedMemoryRanges(m_Device, 1, &rebasedRange); RDCASSERTEQUAL(vkr, VK_SUCCESS); m_pDriver->vkUnmapMemory(m_Device, rebasedIdxBufMem); } uint32_t bufStride = 0; std::vector modSpirv = moduleInfo.spirv.spirv; struct CompactedAttrBuffer { VkDeviceMemory mem; VkBuffer buf; VkBufferView view; }; std::vector attrInstDivisor; CompactedAttrBuffer vbuffers[64]; RDCEraseEl(vbuffers); { VkWriteDescriptorSet descWrites[64]; uint32_t numWrites = 0; RDCEraseEl(descWrites); const VkPipelineVertexInputStateCreateInfo *vi = pipeCreateInfo.pVertexInputState; RDCASSERT(vi->vertexAttributeDescriptionCount <= MeshOutputTBufferArraySize); // we fetch the vertex buffer data up front here since there's a very high chance of either // overlap due to interleaved attributes, or no overlap and no wastage due to separate compact // attributes. std::vector origVBs; origVBs.reserve(16); for(uint32_t vb = 0; vb < vi->vertexBindingDescriptionCount; vb++) { uint32_t binding = vi->pVertexBindingDescriptions[vb].binding; if(binding >= state.vbuffers.size()) { origVBs.push_back(bytebuf()); continue; } VkDeviceSize offs = state.vbuffers[binding].offs; uint64_t len = 0; if(vi->pVertexBindingDescriptions[vb].inputRate == VK_VERTEX_INPUT_RATE_INSTANCE) { len = uint64_t(maxInstance + 1) * vi->pVertexBindingDescriptions[vb].stride; offs += drawcall->instanceOffset * vi->pVertexBindingDescriptions[vb].stride; } else { len = uint64_t(maxIndex + 1) * vi->pVertexBindingDescriptions[vb].stride; offs += drawcall->vertexOffset * vi->pVertexBindingDescriptions[vb].stride; } if(state.vbuffers[binding].buf != ResourceId()) { origVBs.push_back(bytebuf()); GetBufferData(state.vbuffers[binding].buf, offs, len, origVBs.back()); } } for(uint32_t i = 0; i < vi->vertexAttributeDescriptionCount; i++) { const VkVertexInputAttributeDescription &attrDesc = vi->pVertexAttributeDescriptions[i]; uint32_t attr = attrDesc.location; RDCASSERT(attr < 64); if(attr >= ARRAY_COUNT(vbuffers)) { RDCERR("Attribute index too high! Resize array."); continue; } uint32_t instDivisor = ~0U; size_t stride = 1; const byte *origVBBegin = NULL; const byte *origVBEnd = NULL; for(uint32_t vb = 0; vb < vi->vertexBindingDescriptionCount; vb++) { const VkVertexInputBindingDescription &vbDesc = vi->pVertexBindingDescriptions[vb]; if(vbDesc.binding == attrDesc.binding) { origVBBegin = origVBs[vb].data() + attrDesc.offset; origVBEnd = origVBs[vb].data() + origVBs[vb].size(); stride = vbDesc.stride; if(vbDesc.inputRate == VK_VERTEX_INPUT_RATE_INSTANCE) instDivisor = pipeInfo.vertexBindings[vbDesc.binding].instanceDivisor; else instDivisor = ~0U; break; } } RDCASSERT(origVBEnd); // in some limited cases, provided we added the UNIFORM_TEXEL_BUFFER usage bit, we could use // the original buffers here as-is and read out of them. However it is likely that the offset // is not a multiple of the minimum texel buffer offset for at least some of the buffers if // not all of them, so we simplify the code here by *always* reading back the vertex buffer // data and uploading a compacted version. // we also need to handle the case where the format is not natively supported as a texel // buffer, which requires us to then pick a supported format that's wider (so contains the // same precision) but does support texel buffers, and expand to that. VkFormat origFormat = attrDesc.format; VkFormat expandedFormat = attrDesc.format; if((m_pDriver->GetFormatProperties(attrDesc.format).bufferFeatures & VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT) == 0) { // Our selection is simple. For integer formats, the 4-component version is spec-required to // be supported, so we can expand to that and just pad/upcast the data directly. // Likewise for float formats, the 4-component 32-bit float version is required to be // supported, and can represent any other float format (e.g. R16_SNORM can't be represented // by R16_SFLOAT but can be represented by R32_SFLOAT. Same for R16_*SCALED. Fortunately // there is no R32_SNORM or R32_*SCALED). // So we pick one of three formats depending on the base type of the original format. // // Note: This does not handle double format inputs, which must have special handling. if(IsDoubleFormat(origFormat)) expandedFormat = VK_FORMAT_R32G32B32A32_UINT; else if(IsUIntFormat(origFormat)) expandedFormat = VK_FORMAT_R32G32B32A32_UINT; else if(IsSIntFormat(origFormat)) expandedFormat = VK_FORMAT_R32G32B32A32_SINT; else expandedFormat = VK_FORMAT_R32G32B32A32_SFLOAT; } uint32_t elemSize = GetByteSize(1, 1, 1, expandedFormat, 0); // doubles are packed as uvec2 if(IsDoubleFormat(origFormat)) elemSize *= 2; // used for interpreting the original data, if we're upcasting ResourceFormat fmt = MakeResourceFormat(origFormat); { VkBufferCreateInfo bufInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, NULL, 0, elemSize * (maxIndex + 1), VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, }; if(instDivisor != ~0U) bufInfo.size = elemSize * (maxInstance + 1); vkr = m_pDriver->vkCreateBuffer(dev, &bufInfo, NULL, &vbuffers[attr].buf); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkMemoryRequirements mrq = {0}; m_pDriver->vkGetBufferMemoryRequirements(dev, vbuffers[attr].buf, &mrq); VkMemoryAllocateInfo allocInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size, m_pDriver->GetUploadMemoryIndex(mrq.memoryTypeBits), }; vkr = m_pDriver->vkAllocateMemory(dev, &allocInfo, NULL, &vbuffers[attr].mem); if(vkr == VK_ERROR_OUT_OF_DEVICE_MEMORY || vkr == VK_ERROR_OUT_OF_HOST_MEMORY) { RDCWARN("Failed to allocate %llu bytes for patched vertex buffer", mrq.size); return; } RDCASSERTEQUAL(vkr, VK_SUCCESS); vkr = m_pDriver->vkBindBufferMemory(dev, vbuffers[attr].buf, vbuffers[attr].mem, 0); RDCASSERTEQUAL(vkr, VK_SUCCESS); byte *compactedData = NULL; vkr = m_pDriver->vkMapMemory(m_Device, vbuffers[attr].mem, 0, VK_WHOLE_SIZE, 0, (void **)&compactedData); RDCASSERTEQUAL(vkr, VK_SUCCESS); if(compactedData && origVBEnd) { const byte *src = origVBBegin; byte *dst = compactedData; const byte *dstEnd = dst + bufInfo.size; // fast memcpy compaction case for natively supported texel buffer formats if(origFormat == expandedFormat) { while(src < origVBEnd && dst < dstEnd) { memcpy(dst, src, elemSize); dst += elemSize; src += stride; } } else { uint32_t zero = 0; // upcasting path if(IsDoubleFormat(origFormat)) { while(src < origVBEnd && dst < dstEnd) { // the double is already in "packed uvec2" order, with least significant 32-bits // first, so we can copy directly memcpy(dst, src, sizeof(double) * fmt.compCount); dst += sizeof(double) * fmt.compCount; // fill up to *8* zeros not 4, since we're filling two for every component for(uint8_t c = fmt.compCount * 2; c < 8; c++) { memcpy(dst, &zero, sizeof(uint32_t)); dst += sizeof(uint32_t); } src += stride; } } else if(IsUIntFormat(expandedFormat)) { while(src < origVBEnd && dst < dstEnd) { uint32_t val = 0; const byte *s = src; uint8_t c = 0; for(; c < fmt.compCount; c++) { if(fmt.compByteWidth == 1) val = *s; else if(fmt.compByteWidth == 2) val = *(uint16_t *)s; else if(fmt.compByteWidth == 4) val = *(uint32_t *)s; memcpy(dst, &val, sizeof(uint32_t)); dst += sizeof(uint32_t); s += fmt.compByteWidth; } for(; c < 4; c++) { memcpy(dst, &zero, sizeof(uint32_t)); dst += sizeof(uint32_t); } src += stride; } } else if(IsSIntFormat(expandedFormat)) { while(src < origVBEnd && dst < dstEnd) { int32_t val = 0; const byte *s = src; uint8_t c = 0; for(; c < fmt.compCount; c++) { if(fmt.compByteWidth == 1) val = *(int8_t *)s; else if(fmt.compByteWidth == 2) val = *(int16_t *)s; else if(fmt.compByteWidth == 4) val = *(int32_t *)s; memcpy(dst, &val, sizeof(int32_t)); dst += sizeof(int32_t); s += fmt.compByteWidth; } for(; c < 4; c++) { memcpy(dst, &zero, sizeof(uint32_t)); dst += sizeof(uint32_t); } src += stride; } } else { while(src < origVBEnd && dst < dstEnd) { bool valid = false; FloatVector vec = HighlightCache::InterpretVertex(src, 0, 0, fmt, origVBEnd, valid); memcpy(dst, &vec, sizeof(FloatVector)); dst += sizeof(FloatVector); src += stride; } } } } VkMappedMemoryRange range = { VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, NULL, vbuffers[attr].mem, 0, VK_WHOLE_SIZE, }; vkr = m_pDriver->vkFlushMappedMemoryRanges(m_Device, 1, &range); RDCASSERTEQUAL(vkr, VK_SUCCESS); m_pDriver->vkUnmapMemory(m_Device, vbuffers[attr].mem); } VkBufferViewCreateInfo info = { VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO, NULL, 0, vbuffers[attr].buf, expandedFormat, 0, VK_WHOLE_SIZE, }; if((m_pDriver->GetFormatProperties(expandedFormat).bufferFeatures & VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT) == 0) { RDCERR( "Format %s doesn't support texel buffers, and no suitable upcasting format was found! " "Replacing with safe but broken format to avoid crashes, but vertex data will be " "wrong.", ToStr(origFormat).c_str()); info.format = VK_FORMAT_R8G8B8A8_UNORM; } m_pDriver->vkCreateBufferView(dev, &info, NULL, &vbuffers[attr].view); attrInstDivisor.resize(RDCMAX(attrInstDivisor.size(), size_t(attr + 1))); attrInstDivisor[attr] = instDivisor; descWrites[numWrites].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; descWrites[numWrites].dstSet = descSets[0]; if(IsSIntFormat(attrDesc.format)) descWrites[numWrites].dstBinding = 4; else if(IsUIntFormat(attrDesc.format) || IsDoubleFormat(attrDesc.format)) descWrites[numWrites].dstBinding = 3; else descWrites[numWrites].dstBinding = 2; descWrites[numWrites].dstArrayElement = attr; descWrites[numWrites].descriptorCount = 1; descWrites[numWrites].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; descWrites[numWrites].pTexelBufferView = &vbuffers[attr].view; numWrites++; } // add a write of the index buffer if(uniqIdxBufView != VK_NULL_HANDLE) { descWrites[numWrites].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; descWrites[numWrites].dstSet = descSets[0]; descWrites[numWrites].dstBinding = 1; descWrites[numWrites].dstArrayElement = 0; descWrites[numWrites].descriptorCount = 1; descWrites[numWrites].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; descWrites[numWrites].pTexelBufferView = &uniqIdxBufView; numWrites++; } m_pDriver->vkUpdateDescriptorSets(dev, numWrites, descWrites, 0, NULL); } ConvertToMeshOutputCompute(*refl, *pipeInfo.shaders[0].patchData, pipeInfo.shaders[0].entryPoint.c_str(), attrInstDivisor, drawcall, numVerts, numViews, modSpirv, bufStride); VkComputePipelineCreateInfo compPipeInfo = {VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; // repoint pipeline layout compPipeInfo.layout = pipeLayout; // create vertex shader with modified code VkShaderModuleCreateInfo moduleCreateInfo = { VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, NULL, 0, modSpirv.size() * sizeof(uint32_t), &modSpirv[0], }; VkShaderModule module; vkr = m_pDriver->vkCreateShaderModule(dev, &moduleCreateInfo, NULL, &module); RDCASSERTEQUAL(vkr, VK_SUCCESS); compPipeInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; compPipeInfo.stage.module = module; compPipeInfo.stage.pName = PatchedMeshOutputEntryPoint; compPipeInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; // copy over specialization info for(uint32_t s = 0; s < pipeCreateInfo.stageCount; s++) { if(pipeCreateInfo.pStages[s].stage == VK_SHADER_STAGE_VERTEX_BIT) { compPipeInfo.stage.pSpecializationInfo = pipeCreateInfo.pStages[s].pSpecializationInfo; break; } } // create new pipeline VkPipeline pipe; vkr = m_pDriver->vkCreateComputePipelines(m_Device, VK_NULL_HANDLE, 1, &compPipeInfo, NULL, &pipe); if(vkr != VK_SUCCESS) { RDCERR("Failed to create patched compute pipeline: %s", ToStr(vkr).c_str()); return; } // make copy of state to draw from VulkanRenderState modifiedstate = state; // bind created pipeline to partial replay state modifiedstate.compute.pipeline = GetResID(pipe); // move graphics descriptor sets onto the compute pipe. modifiedstate.compute.descSets = modifiedstate.graphics.descSets; // replace descriptor set IDs with our temporary sets. The offsets we keep the same. If the // original draw had no sets, we ensure there's room (with no offsets needed) if(modifiedstate.compute.descSets.empty()) modifiedstate.compute.descSets.resize(1); for(size_t i = 0; i < descSets.size(); i++) modifiedstate.compute.descSets[i].descSet = GetResID(descSets[i]); { // create buffer of sufficient size // this can't just be bufStride * num unique indices per instance, as we don't // have a compact 0-based index to index into the buffer. We must use // index-minIndex which is 0-based but potentially sparse, so this buffer may // be more or less wasteful VkBufferCreateInfo bufInfo = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; // set bufSize bufSize = bufInfo.size = uint64_t(numVerts) * uint64_t(drawcall->numInstances) * uint64_t(bufStride) * uint64_t(numViews); bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; bufInfo.usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT; bufInfo.usage |= VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; bufInfo.usage |= VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; vkr = m_pDriver->vkCreateBuffer(dev, &bufInfo, NULL, &meshBuffer); RDCASSERTEQUAL(vkr, VK_SUCCESS); bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; vkr = m_pDriver->vkCreateBuffer(dev, &bufInfo, NULL, &readbackBuffer); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkMemoryRequirements mrq = {0}; m_pDriver->vkGetBufferMemoryRequirements(dev, meshBuffer, &mrq); VkMemoryAllocateInfo allocInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size, m_pDriver->GetGPULocalMemoryIndex(mrq.memoryTypeBits), }; vkr = m_pDriver->vkAllocateMemory(dev, &allocInfo, NULL, &meshMem); if(vkr == VK_ERROR_OUT_OF_DEVICE_MEMORY || vkr == VK_ERROR_OUT_OF_HOST_MEMORY) { RDCWARN("Failed to allocate %llu bytes for output vertex SSBO", mrq.size); return; } RDCASSERTEQUAL(vkr, VK_SUCCESS); vkr = m_pDriver->vkBindBufferMemory(dev, meshBuffer, meshMem, 0); RDCASSERTEQUAL(vkr, VK_SUCCESS); m_pDriver->vkGetBufferMemoryRequirements(dev, readbackBuffer, &mrq); allocInfo.memoryTypeIndex = m_pDriver->GetReadbackMemoryIndex(mrq.memoryTypeBits); vkr = m_pDriver->vkAllocateMemory(dev, &allocInfo, NULL, &readbackMem); if(vkr == VK_ERROR_OUT_OF_DEVICE_MEMORY || vkr == VK_ERROR_OUT_OF_HOST_MEMORY) { RDCWARN("Failed to allocate %llu bytes for readback memory", mrq.size); return; } RDCASSERTEQUAL(vkr, VK_SUCCESS); vkr = m_pDriver->vkBindBufferMemory(dev, readbackBuffer, readbackMem, 0); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkCommandBuffer cmd = m_pDriver->GetNextCmd(); VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; vkr = ObjDisp(dev)->BeginCommandBuffer(Unwrap(cmd), &beginInfo); RDCASSERTEQUAL(vkr, VK_SUCCESS); // fill destination buffer with 0s to ensure unwritten vertices have sane data ObjDisp(dev)->CmdFillBuffer(Unwrap(cmd), Unwrap(meshBuffer), 0, bufInfo.size, 0); VkBufferMemoryBarrier meshbufbarrier = { VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, NULL, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, }; meshbufbarrier.size = VK_WHOLE_SIZE; VkMemoryBarrier globalbarrier = { VK_STRUCTURE_TYPE_MEMORY_BARRIER, NULL, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, }; // wait for uploads of index buffer (if used), compacted vertex buffers, and the above fill to // finish. DoPipelineBarrier(cmd, 1, &globalbarrier); // vkUpdateDescriptorSet desc set to point to buffer VkDescriptorBufferInfo fetchdesc = {0}; fetchdesc.buffer = meshBuffer; fetchdesc.offset = 0; fetchdesc.range = bufInfo.size; VkWriteDescriptorSet write = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, NULL, descSets[0], 0, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, NULL, &fetchdesc, NULL}; m_pDriver->vkUpdateDescriptorSets(dev, 1, &write, 0, NULL); // do single draw modifiedstate.BindPipeline(cmd, VulkanRenderState::BindCompute, true); uint64_t totalVerts = numVerts * uint64_t(drawcall->numInstances) * uint64_t(numViews); // the validation layers will probably complain about this dispatch saying some arrays aren't // fully updated. That's because they don't statically analyse that only fixed indices are // referred to. It's safe to leave unused array indices as invalid descriptors. ObjDisp(cmd)->CmdDispatch(Unwrap(cmd), uint32_t(totalVerts / MeshOutputDispatchWidth) + 1, 1, 1); // wait for mesh output writing to finish meshbufbarrier.buffer = Unwrap(meshBuffer); meshbufbarrier.size = bufSize; meshbufbarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; meshbufbarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; DoPipelineBarrier(cmd, 1, &meshbufbarrier); VkBufferCopy bufcopy = { 0, 0, bufInfo.size, }; // copy to readback buffer ObjDisp(dev)->CmdCopyBuffer(Unwrap(cmd), Unwrap(meshBuffer), Unwrap(readbackBuffer), 1, &bufcopy); meshbufbarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; meshbufbarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; meshbufbarrier.buffer = Unwrap(readbackBuffer); // wait for copy to finish DoPipelineBarrier(cmd, 1, &meshbufbarrier); vkr = ObjDisp(dev)->EndCommandBuffer(Unwrap(cmd)); RDCASSERTEQUAL(vkr, VK_SUCCESS); // submit & flush so that we don't have to keep pipeline around for a while m_pDriver->SubmitCmds(); m_pDriver->FlushQ(); } for(CompactedAttrBuffer attrBuf : vbuffers) { m_pDriver->vkDestroyBufferView(dev, attrBuf.view, NULL); m_pDriver->vkDestroyBuffer(dev, attrBuf.buf, NULL); m_pDriver->vkFreeMemory(dev, attrBuf.mem, NULL); } // readback mesh data byte *byteData = NULL; vkr = m_pDriver->vkMapMemory(m_Device, readbackMem, 0, VK_WHOLE_SIZE, 0, (void **)&byteData); VkMappedMemoryRange range = { VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, NULL, readbackMem, 0, VK_WHOLE_SIZE, }; vkr = m_pDriver->vkInvalidateMappedMemoryRanges(m_Device, 1, &range); RDCASSERTEQUAL(vkr, VK_SUCCESS); // do near/far calculations float nearp = 0.1f; float farp = 100.0f; Vec4f *pos0 = (Vec4f *)byteData; bool found = false; // expect position at the start of the buffer, as system values are sorted first // and position is the first value for(uint32_t i = 1; refl->outputSignature[0].systemValue == ShaderBuiltin::Position && i < numVerts; i++) { ////////////////////////////////////////////////////////////////////////////////// // derive near/far, assuming a standard perspective matrix // // the transformation from from pre-projection {Z,W} to post-projection {Z,W} // is linear. So we can say Zpost = Zpre*m + c . Here we assume Wpre = 1 // and we know Wpost = Zpre from the perspective matrix. // we can then see from the perspective matrix that // m = F/(F-N) // c = -(F*N)/(F-N) // // with re-arranging and substitution, we then get: // N = -c/m // F = c/(1-m) // // so if we can derive m and c then we can determine N and F. We can do this with // two points, and we pick them reasonably distinct on z to reduce floating-point // error Vec4f *pos = (Vec4f *)(byteData + i * bufStride); // skip invalid vertices (w=0) if(pos->w != 0.0f && fabs(pos->w - pos0->w) > 0.01f && fabs(pos->z - pos0->z) > 0.01f) { Vec2f A(pos0->w, pos0->z); Vec2f B(pos->w, pos->z); float m = (B.y - A.y) / (B.x - A.x); float c = B.y - B.x * m; if(m == 1.0f) continue; if(-c / m <= 0.000001f) continue; nearp = -c / m; farp = c / (1 - m); found = true; break; } } // if we didn't find anything, all z's and w's were identical. // If the z is positive and w greater for the first element then // we detect this projection as reversed z with infinite far plane if(!found && pos0->z > 0.0f && pos0->w > pos0->z) { nearp = pos0->z; farp = FLT_MAX; } m_pDriver->vkUnmapMemory(m_Device, readbackMem); // clean up temporary memories m_pDriver->vkDestroyBuffer(m_Device, readbackBuffer, NULL); m_pDriver->vkFreeMemory(m_Device, readbackMem, NULL); if(uniqIdxBuf != VK_NULL_HANDLE) { m_pDriver->vkDestroyBuffer(m_Device, uniqIdxBuf, NULL); m_pDriver->vkFreeMemory(m_Device, uniqIdxBufMem, NULL); m_pDriver->vkDestroyBufferView(m_Device, uniqIdxBufView, NULL); } // fill out m_PostVS.Data m_PostVS.Data[eventId].vsin.topo = pipeCreateInfo.pInputAssemblyState->topology; m_PostVS.Data[eventId].vsout.topo = pipeCreateInfo.pInputAssemblyState->topology; m_PostVS.Data[eventId].vsout.buf = meshBuffer; m_PostVS.Data[eventId].vsout.bufmem = meshMem; m_PostVS.Data[eventId].vsout.baseVertex = 0; m_PostVS.Data[eventId].vsout.numViews = numViews; m_PostVS.Data[eventId].vsout.vertStride = bufStride; m_PostVS.Data[eventId].vsout.nearPlane = nearp; m_PostVS.Data[eventId].vsout.farPlane = farp; m_PostVS.Data[eventId].vsout.useIndices = bool(drawcall->flags & DrawFlags::Indexed); m_PostVS.Data[eventId].vsout.numVerts = drawcall->numIndices; m_PostVS.Data[eventId].vsout.instStride = 0; if(drawcall->flags & DrawFlags::Instanced) m_PostVS.Data[eventId].vsout.instStride = uint32_t(bufSize / (drawcall->numInstances * numViews)); m_PostVS.Data[eventId].vsout.idxbuf = VK_NULL_HANDLE; if(m_PostVS.Data[eventId].vsout.useIndices && state.ibuffer.buf != ResourceId()) { m_PostVS.Data[eventId].vsout.idxbuf = rebasedIdxBuf; m_PostVS.Data[eventId].vsout.idxbufmem = rebasedIdxBufMem; m_PostVS.Data[eventId].vsout.idxFmt = idxsize == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32; } m_PostVS.Data[eventId].vsout.hasPosOut = refl->outputSignature[0].systemValue == ShaderBuiltin::Position; // delete descriptors. Technically we don't have to free the descriptor sets, but our tracking on // replay doesn't handle destroying children of pooled objects so we do it explicitly anyway. m_pDriver->vkFreeDescriptorSets(dev, descpool, (uint32_t)descSets.size(), descSets.data()); m_pDriver->vkDestroyDescriptorPool(dev, descpool, NULL); for(VkDescriptorSetLayout layout : setLayouts) m_pDriver->vkDestroyDescriptorSetLayout(dev, layout, NULL); // delete pipeline layout m_pDriver->vkDestroyPipelineLayout(dev, pipeLayout, NULL); // delete pipeline m_pDriver->vkDestroyPipeline(dev, pipe, NULL); // delete shader/shader module m_pDriver->vkDestroyShaderModule(dev, module, NULL); } void VulkanReplay::FetchTessGSOut(uint32_t eventId) { VulkanRenderState state = m_pDriver->m_RenderState; VulkanCreationInfo &creationInfo = m_pDriver->m_CreationInfo; const VulkanCreationInfo::Pipeline &pipeInfo = creationInfo.m_Pipeline[state.graphics.pipeline]; const DrawcallDescription *drawcall = m_pDriver->GetDrawcall(eventId); // set defaults so that we don't try to fetch this output again if something goes wrong and the // same event is selected again { m_PostVS.Data[eventId].gsout.buf = VK_NULL_HANDLE; m_PostVS.Data[eventId].gsout.bufmem = VK_NULL_HANDLE; m_PostVS.Data[eventId].gsout.instStride = 0; m_PostVS.Data[eventId].gsout.vertStride = 0; m_PostVS.Data[eventId].gsout.numViews = 1; m_PostVS.Data[eventId].gsout.nearPlane = 0.0f; m_PostVS.Data[eventId].gsout.farPlane = 0.0f; m_PostVS.Data[eventId].gsout.useIndices = false; m_PostVS.Data[eventId].gsout.hasPosOut = false; m_PostVS.Data[eventId].gsout.idxbuf = VK_NULL_HANDLE; m_PostVS.Data[eventId].gsout.idxbufmem = VK_NULL_HANDLE; } if(!creationInfo.m_RenderPass[state.renderPass].subpasses[state.subpass].multiviews.empty()) { RDCWARN("Multipass is active for this draw, no GS/Tess mesh output is available"); return; } // first try geometry stage int stageIndex = 3; // if there is no such shader bound, try tessellation if(!pipeInfo.shaders[stageIndex].refl) stageIndex = 2; // if still nothing, do vertex if(!pipeInfo.shaders[stageIndex].refl) stageIndex = 0; ShaderReflection *lastRefl = pipeInfo.shaders[stageIndex].refl; RDCASSERT(lastRefl); uint32_t primitiveMultiplier = 1; // transform feedback expands strips to lists switch(pipeInfo.shaders[stageIndex].patchData->outTopo) { case Topology::PointList: m_PostVS.Data[eventId].gsout.topo = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; break; case Topology::LineList: case Topology::LineStrip: m_PostVS.Data[eventId].gsout.topo = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; primitiveMultiplier = 2; break; default: RDCERR("Unexpected output topology %s", ToStr(pipeInfo.shaders[stageIndex].patchData->outTopo).c_str()); // deliberate fallthrough case Topology::TriangleList: case Topology::TriangleStrip: m_PostVS.Data[eventId].gsout.topo = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; primitiveMultiplier = 3; break; } if(lastRefl->outputSignature.empty()) { // empty vertex output signature m_PostVS.Data[eventId].gsout.buf = VK_NULL_HANDLE; m_PostVS.Data[eventId].gsout.bufmem = VK_NULL_HANDLE; m_PostVS.Data[eventId].gsout.instStride = 0; m_PostVS.Data[eventId].gsout.vertStride = 0; m_PostVS.Data[eventId].gsout.numViews = 1; m_PostVS.Data[eventId].gsout.nearPlane = 0.0f; m_PostVS.Data[eventId].gsout.farPlane = 0.0f; m_PostVS.Data[eventId].gsout.useIndices = false; m_PostVS.Data[eventId].gsout.hasPosOut = false; m_PostVS.Data[eventId].gsout.idxbuf = VK_NULL_HANDLE; m_PostVS.Data[eventId].gsout.idxbufmem = VK_NULL_HANDLE; return; } if(!ObjDisp(m_Device)->CmdBeginTransformFeedbackEXT) { RDCLOG( "VK_EXT_transform_feedback extension not available, can't fetch tessellation/geometry " "output"); return; } const VulkanCreationInfo::ShaderModule &moduleInfo = creationInfo.m_ShaderModule[pipeInfo.shaders[stageIndex].module]; std::vector modSpirv = moduleInfo.spirv.spirv; uint32_t xfbStride = 0; // adds XFB annotations in order of the output signature (with the position first) AddXFBAnnotations(*lastRefl, *pipeInfo.shaders[stageIndex].patchData, pipeInfo.shaders[stageIndex].entryPoint.c_str(), modSpirv, xfbStride); // create vertex shader with modified code VkShaderModuleCreateInfo moduleCreateInfo = { VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, NULL, 0, modSpirv.size() * sizeof(uint32_t), &modSpirv[0], }; VkResult vkr = VK_SUCCESS; VkDevice dev = m_Device; VkShaderModule module; vkr = m_pDriver->vkCreateShaderModule(dev, &moduleCreateInfo, NULL, &module); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkGraphicsPipelineCreateInfo pipeCreateInfo; // get pipeline create info m_pDriver->GetShaderCache()->MakeGraphicsPipelineInfo(pipeCreateInfo, state.graphics.pipeline); VkPipelineRasterizationStateCreateInfo *rs = (VkPipelineRasterizationStateCreateInfo *)pipeCreateInfo.pRasterizationState; rs->rasterizerDiscardEnable = true; for(uint32_t i = 0; i < pipeCreateInfo.stageCount; i++) { VkPipelineShaderStageCreateInfo &stage = (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[i]; if(StageIndex(stage.stage) == stageIndex) { stage.module = module; break; } } // create a empty renderpass and framebuffer so we can draw VkFramebuffer fb = VK_NULL_HANDLE; VkRenderPass rp = VK_NULL_HANDLE; VkSubpassDescription sub = {0, VK_PIPELINE_BIND_POINT_GRAPHICS}; VkRenderPassCreateInfo rpinfo = { VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, NULL, 0, 0, NULL, 1, &sub, }; vkr = m_pDriver->vkCreateRenderPass(m_Device, &rpinfo, NULL, &rp); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkFramebufferCreateInfo fbinfo = { VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, NULL, 0, rp, 0, NULL, 16U, 16U, 1, }; vkr = m_pDriver->vkCreateFramebuffer(m_Device, &fbinfo, NULL, &fb); RDCASSERTEQUAL(vkr, VK_SUCCESS); pipeCreateInfo.renderPass = rp; pipeCreateInfo.subpass = 0; VkPipeline pipe = VK_NULL_HANDLE; vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo, NULL, &pipe); RDCASSERTEQUAL(vkr, VK_SUCCESS); state.graphics.pipeline = GetResID(pipe); state.framebuffer = GetResID(fb); state.renderPass = GetResID(rp); state.subpass = 0; state.renderArea.offset.x = 0; state.renderArea.offset.y = 0; state.renderArea.extent.width = 16; state.renderArea.extent.height = 16; // disable any existing XFB state.xfbbuffers.clear(); state.xfbcounters.clear(); if(m_PostVS.XFBQueryPoolSize < drawcall->numInstances) { if(m_PostVS.XFBQueryPoolSize != VK_NULL_HANDLE) m_pDriver->vkDestroyQueryPool(m_Device, m_PostVS.XFBQueryPool, NULL); VkQueryPoolCreateInfo info = { VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, NULL, 0, VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT, drawcall->numInstances, 0, }; vkr = m_pDriver->vkCreateQueryPool(m_Device, &info, NULL, &m_PostVS.XFBQueryPool); RDCASSERTEQUAL(vkr, VK_SUCCESS); m_PostVS.XFBQueryPoolSize = drawcall->numInstances; } VkBuffer meshBuffer = VK_NULL_HANDLE; VkDeviceMemory meshMem = VK_NULL_HANDLE; // start with bare minimum size, which might be enough if no expansion happens VkDeviceSize bufferSize = 0; VkDeviceSize dataSize = uint64_t(drawcall->numIndices) * uint64_t(drawcall->numInstances) * uint64_t(xfbStride); VkXfbQueryResult queryResult = {}; while(bufferSize < dataSize) { bufferSize = dataSize; if(meshBuffer != VK_NULL_HANDLE) { m_pDriver->vkDestroyBuffer(dev, meshBuffer, NULL); m_pDriver->vkFreeMemory(dev, meshMem, NULL); meshBuffer = VK_NULL_HANDLE; meshMem = VK_NULL_HANDLE; } VkBufferCreateInfo bufInfo = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; bufInfo.size = bufferSize; bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; bufInfo.usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT; bufInfo.usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT; bufInfo.usage |= VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; vkr = m_pDriver->vkCreateBuffer(dev, &bufInfo, NULL, &meshBuffer); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkMemoryRequirements mrq = {0}; m_pDriver->vkGetBufferMemoryRequirements(dev, meshBuffer, &mrq); VkMemoryAllocateInfo allocInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size, m_pDriver->GetGPULocalMemoryIndex(mrq.memoryTypeBits), }; vkr = m_pDriver->vkAllocateMemory(dev, &allocInfo, NULL, &meshMem); if(vkr == VK_ERROR_OUT_OF_DEVICE_MEMORY || vkr == VK_ERROR_OUT_OF_HOST_MEMORY) { RDCWARN("Output allocation for %llu bytes failed fetching tessellation/geometry output.", mrq.size); m_pDriver->vkDestroyBuffer(dev, meshBuffer, NULL); // delete framebuffer and renderpass m_pDriver->vkDestroyFramebuffer(dev, fb, NULL); m_pDriver->vkDestroyRenderPass(dev, rp, NULL); // delete pipeline m_pDriver->vkDestroyPipeline(dev, pipe, NULL); // delete shader/shader module m_pDriver->vkDestroyShaderModule(dev, module, NULL); return; } RDCASSERTEQUAL(vkr, VK_SUCCESS); vkr = m_pDriver->vkBindBufferMemory(dev, meshBuffer, meshMem, 0); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkCommandBuffer cmd = m_pDriver->GetNextCmd(); VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; vkr = ObjDisp(dev)->BeginCommandBuffer(Unwrap(cmd), &beginInfo); RDCASSERTEQUAL(vkr, VK_SUCCESS); ObjDisp(dev)->CmdResetQueryPool(Unwrap(cmd), Unwrap(m_PostVS.XFBQueryPool), 0, 1); // fill destination buffer with 0s to ensure unwritten vertices have sane data ObjDisp(dev)->CmdFillBuffer(Unwrap(cmd), Unwrap(meshBuffer), 0, bufInfo.size, 0); VkBufferMemoryBarrier meshbufbarrier = { VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, NULL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, Unwrap(meshBuffer), 0, bufInfo.size, }; // wait for the above fill to finish. DoPipelineBarrier(cmd, 1, &meshbufbarrier); state.BeginRenderPassAndApplyState(cmd, VulkanRenderState::BindGraphics); ObjDisp(cmd)->CmdBeginQuery(Unwrap(cmd), Unwrap(m_PostVS.XFBQueryPool), 0, 0); ObjDisp(cmd)->CmdBindTransformFeedbackBuffersEXT(Unwrap(cmd), 0, 1, UnwrapPtr(meshBuffer), &meshbufbarrier.offset, &meshbufbarrier.size); ObjDisp(cmd)->CmdBeginTransformFeedbackEXT(Unwrap(cmd), 0, 1, NULL, NULL); if(drawcall->flags & DrawFlags::Indexed) { ObjDisp(cmd)->CmdDrawIndexed(Unwrap(cmd), drawcall->numIndices, drawcall->numInstances, drawcall->indexOffset, drawcall->baseVertex, drawcall->instanceOffset); } else { ObjDisp(cmd)->CmdDraw(Unwrap(cmd), drawcall->numIndices, drawcall->numInstances, drawcall->vertexOffset, drawcall->instanceOffset); } ObjDisp(cmd)->CmdEndTransformFeedbackEXT(Unwrap(cmd), 0, 1, NULL, NULL); ObjDisp(cmd)->CmdEndQuery(Unwrap(cmd), Unwrap(m_PostVS.XFBQueryPool), 0); state.EndRenderPass(cmd); vkr = ObjDisp(dev)->EndCommandBuffer(Unwrap(cmd)); RDCASSERTEQUAL(vkr, VK_SUCCESS); m_pDriver->SubmitCmds(); m_pDriver->FlushQ(); vkr = ObjDisp(dev)->GetQueryPoolResults( Unwrap(dev), Unwrap(m_PostVS.XFBQueryPool), 0, 1, sizeof(VkXfbQueryResult), &queryResult, sizeof(VkXfbQueryResult), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); RDCASSERTEQUAL(vkr, VK_SUCCESS); VkDeviceSize generatedSize = queryResult.numPrimitivesGenerated * 3 * xfbStride; // output buffer isn't big enough, delete it and re-run so we recreate it larger if(generatedSize > dataSize) dataSize = generatedSize; } std::vector instData; // instanced draws must be replayed one at a time so we can record the number of primitives from // each drawcall, as due to expansion this can vary per-instance. if(drawcall->flags & DrawFlags::Instanced && drawcall->numInstances > 1) { VkCommandBuffer cmd = m_pDriver->GetNextCmd(); VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; vkr = ObjDisp(dev)->BeginCommandBuffer(Unwrap(cmd), &beginInfo); RDCASSERTEQUAL(vkr, VK_SUCCESS); ObjDisp(dev)->CmdResetQueryPool(Unwrap(cmd), Unwrap(m_PostVS.XFBQueryPool), 0, drawcall->numInstances); state.BeginRenderPassAndApplyState(cmd, VulkanRenderState::BindGraphics); // do incremental draws to get the output size. We have to do this O(N^2) style because // there's no way to replay only a single instance. We have to replay 1, 2, 3, ... N // instances and count the total number of verts each time, then we can see from the // difference how much each instance wrote. for(uint32_t inst = 1; inst <= drawcall->numInstances; inst++) { ObjDisp(cmd)->CmdBeginQuery(Unwrap(cmd), Unwrap(m_PostVS.XFBQueryPool), inst - 1, 0); VkDeviceSize offset = 0; ObjDisp(cmd)->CmdBindTransformFeedbackBuffersEXT(Unwrap(cmd), 0, 1, UnwrapPtr(meshBuffer), &offset, &bufferSize); ObjDisp(cmd)->CmdBeginTransformFeedbackEXT(Unwrap(cmd), 0, 1, NULL, NULL); if(drawcall->flags & DrawFlags::Indexed) { ObjDisp(cmd)->CmdDrawIndexed(Unwrap(cmd), drawcall->numIndices, inst, drawcall->indexOffset, drawcall->baseVertex, drawcall->instanceOffset); } else { ObjDisp(cmd)->CmdDraw(Unwrap(cmd), drawcall->numIndices, inst, drawcall->vertexOffset, drawcall->instanceOffset); } ObjDisp(cmd)->CmdEndTransformFeedbackEXT(Unwrap(cmd), 0, 1, NULL, NULL); ObjDisp(cmd)->CmdEndQuery(Unwrap(cmd), Unwrap(m_PostVS.XFBQueryPool), inst - 1); } state.EndRenderPass(cmd); vkr = ObjDisp(dev)->EndCommandBuffer(Unwrap(cmd)); RDCASSERTEQUAL(vkr, VK_SUCCESS); m_pDriver->SubmitCmds(); m_pDriver->FlushQ(); std::vector queryResults; queryResults.resize(drawcall->numInstances); vkr = ObjDisp(dev)->GetQueryPoolResults( Unwrap(dev), Unwrap(m_PostVS.XFBQueryPool), 0, drawcall->numInstances, sizeof(VkXfbQueryResult) * drawcall->numInstances, queryResults.data(), sizeof(VkXfbQueryResult), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); RDCASSERTEQUAL(vkr, VK_SUCCESS); uint64_t prevVertCount = 0; for(uint32_t inst = 0; inst < drawcall->numInstances; inst++) { uint64_t vertCount = queryResults[inst].numPrimitivesWritten * primitiveMultiplier; VulkanPostVSData::InstData d; d.numVerts = uint32_t(vertCount - prevVertCount); d.bufOffset = uint32_t(xfbStride * prevVertCount); prevVertCount = vertCount; instData.push_back(d); } } float nearp = 0.1f; float farp = 100.0f; Vec4f pos0; bool found = false; // we read back the buffer in chunks, since we're likely to find a match in the first few // vertices. VkDeviceSize readbackoffset = 0; const VkDeviceSize readbacksize = 1024 * 1024; while(readbackoffset < bufferSize) { bytebuf data; GetBufferData(GetResID(meshBuffer), readbackoffset, readbacksize, data); if(data.empty()) break; if(readbackoffset == 0) memcpy(&pos0, data.data(), sizeof(pos0)); for(uint32_t i = 0; i < data.size() / xfbStride; i++) { ////////////////////////////////////////////////////////////////////////////////// // derive near/far, assuming a standard perspective matrix // // the transformation from from pre-projection {Z,W} to post-projection {Z,W} // is linear. So we can say Zpost = Zpre*m + c . Here we assume Wpre = 1 // and we know Wpost = Zpre from the perspective matrix. // we can then see from the perspective matrix that // m = F/(F-N) // c = -(F*N)/(F-N) // // with re-arranging and substitution, we then get: // N = -c/m // F = c/(1-m) // // so if we can derive m and c then we can determine N and F. We can do this with // two points, and we pick them reasonably distinct on z to reduce floating-point // error Vec4f *pos = (Vec4f *)(data.data() + xfbStride * i); // skip invalid vertices (w=0) if(pos->w != 0.0f && fabs(pos->w - pos0.w) > 0.01f && fabs(pos->z - pos0.z) > 0.01f) { Vec2f A(pos0.w, pos0.z); Vec2f B(pos->w, pos->z); float m = (B.y - A.y) / (B.x - A.x); float c = B.y - B.x * m; if(m == 1.0f) continue; if(-c / m <= 0.000001f) continue; nearp = -c / m; farp = c / (1 - m); found = true; break; } } if(found) break; // read the next segment readbackoffset += readbacksize; } // if we didn't find anything, all z's and w's were identical. // If the z is positive and w greater for the first element then // we detect this projection as reversed z with infinite far plane if(!found && pos0.z > 0.0f && pos0.w > pos0.z) { nearp = pos0.z; farp = FLT_MAX; } // fill out m_PostVS.Data m_PostVS.Data[eventId].gsout.buf = meshBuffer; m_PostVS.Data[eventId].gsout.bufmem = meshMem; m_PostVS.Data[eventId].gsout.baseVertex = 0; m_PostVS.Data[eventId].gsout.numViews = 1; m_PostVS.Data[eventId].gsout.vertStride = xfbStride; m_PostVS.Data[eventId].gsout.nearPlane = nearp; m_PostVS.Data[eventId].gsout.farPlane = farp; m_PostVS.Data[eventId].gsout.useIndices = false; m_PostVS.Data[eventId].gsout.numVerts = uint32_t(queryResult.numPrimitivesWritten) * primitiveMultiplier; // set instance stride to 0. If there's any stride needed, it will be calculated using instData m_PostVS.Data[eventId].gsout.instStride = 0; m_PostVS.Data[eventId].gsout.instData = instData; m_PostVS.Data[eventId].gsout.idxbuf = VK_NULL_HANDLE; m_PostVS.Data[eventId].gsout.idxbufmem = VK_NULL_HANDLE; m_PostVS.Data[eventId].gsout.hasPosOut = true; // delete framebuffer and renderpass m_pDriver->vkDestroyFramebuffer(dev, fb, NULL); m_pDriver->vkDestroyRenderPass(dev, rp, NULL); // delete pipeline m_pDriver->vkDestroyPipeline(dev, pipe, NULL); // delete shader/shader module m_pDriver->vkDestroyShaderModule(dev, module, NULL); } void VulkanReplay::InitPostVSBuffers(uint32_t eventId) { // go through any aliasing if(m_PostVS.Alias.find(eventId) != m_PostVS.Alias.end()) eventId = m_PostVS.Alias[eventId]; if(m_PostVS.Data.find(eventId) != m_PostVS.Data.end()) return; const VulkanRenderState &state = m_pDriver->m_RenderState; VulkanCreationInfo &creationInfo = m_pDriver->m_CreationInfo; if(state.graphics.pipeline == ResourceId() || state.renderPass == ResourceId()) return; const VulkanCreationInfo::Pipeline &pipeInfo = creationInfo.m_Pipeline[state.graphics.pipeline]; if(pipeInfo.shaders[0].module == ResourceId()) return; const DrawcallDescription *drawcall = m_pDriver->GetDrawcall(eventId); if(drawcall == NULL || drawcall->numIndices == 0 || drawcall->numInstances == 0) return; VkMarkerRegion::Begin(StringFormat::Fmt("FetchVSOut for %u", eventId)); FetchVSOut(eventId); VkMarkerRegion::End(); // if there's no tessellation or geometry shader active, bail out now if(pipeInfo.shaders[2].module == ResourceId() && pipeInfo.shaders[3].module == ResourceId()) return; VkMarkerRegion::Begin(StringFormat::Fmt("FetchTessGSOut for %u", eventId)); FetchTessGSOut(eventId); VkMarkerRegion::End(); } struct VulkanInitPostVSCallback : public VulkanDrawcallCallback { VulkanInitPostVSCallback(WrappedVulkan *vk, const std::vector &events) : m_pDriver(vk), m_Events(events) { m_pDriver->SetDrawcallCB(this); } ~VulkanInitPostVSCallback() { m_pDriver->SetDrawcallCB(NULL); } void PreDraw(uint32_t eid, VkCommandBuffer cmd) { if(std::find(m_Events.begin(), m_Events.end(), eid) != m_Events.end()) m_pDriver->GetReplay()->InitPostVSBuffers(eid); } bool PostDraw(uint32_t eid, VkCommandBuffer cmd) { return false; } void PostRedraw(uint32_t eid, VkCommandBuffer cmd) {} // Dispatches don't rasterize, so do nothing void PreDispatch(uint32_t eid, VkCommandBuffer cmd) {} bool PostDispatch(uint32_t eid, VkCommandBuffer cmd) { return false; } void PostRedispatch(uint32_t eid, VkCommandBuffer cmd) {} // Ditto copy/etc void PreMisc(uint32_t eid, DrawFlags flags, VkCommandBuffer cmd) {} bool PostMisc(uint32_t eid, DrawFlags flags, VkCommandBuffer cmd) { return false; } void PostRemisc(uint32_t eid, DrawFlags flags, VkCommandBuffer cmd) {} void PreEndCommandBuffer(VkCommandBuffer cmd) {} void AliasEvent(uint32_t primary, uint32_t alias) { if(std::find(m_Events.begin(), m_Events.end(), primary) != m_Events.end()) m_pDriver->GetReplay()->AliasPostVSBuffers(primary, alias); } WrappedVulkan *m_pDriver; const std::vector &m_Events; }; void VulkanReplay::InitPostVSBuffers(const std::vector &events) { // first we must replay up to the first event without replaying it. This ensures any // non-command buffer calls like memory unmaps etc all happen correctly before this // command buffer m_pDriver->ReplayLog(0, events.front(), eReplay_WithoutDraw); VulkanInitPostVSCallback cb(m_pDriver, events); // now we replay the events, which are guaranteed (because we generated them in // GetPassEvents above) to come from the same command buffer, so the event IDs are // still locally continuous, even if we jump into replaying. m_pDriver->ReplayLog(events.front(), events.back(), eReplay_Full); } MeshFormat VulkanReplay::GetPostVSBuffers(uint32_t eventId, uint32_t instID, uint32_t viewID, MeshDataStage stage) { // go through any aliasing if(m_PostVS.Alias.find(eventId) != m_PostVS.Alias.end()) eventId = m_PostVS.Alias[eventId]; VulkanPostVSData postvs; RDCEraseEl(postvs); if(m_PostVS.Data.find(eventId) != m_PostVS.Data.end()) postvs = m_PostVS.Data[eventId]; const DrawcallDescription *drawcall = m_pDriver->GetDrawcall(eventId); uint32_t numInstances = 1; if(drawcall && (drawcall->flags & DrawFlags::Instanced)) numInstances = drawcall->numInstances; VulkanPostVSData::StageData s = postvs.GetStage(stage); // clamp viewID if(s.numViews > 1) viewID = RDCMIN(viewID, s.numViews - 1); else viewID = 0; MeshFormat ret; if(s.useIndices && s.idxbuf != VK_NULL_HANDLE) { ret.indexResourceId = GetResID(s.idxbuf); ret.indexByteStride = s.idxFmt == VK_INDEX_TYPE_UINT16 ? 2 : 4; } else { ret.indexResourceId = ResourceId(); ret.indexByteStride = 0; } ret.indexByteOffset = 0; ret.baseVertex = s.baseVertex; if(s.buf != VK_NULL_HANDLE) ret.vertexResourceId = GetResID(s.buf); else ret.vertexResourceId = ResourceId(); ret.vertexByteOffset = s.instStride * (instID + viewID * numInstances); ret.vertexByteStride = s.vertStride; ret.format.compCount = 4; ret.format.compByteWidth = 4; ret.format.compType = CompType::Float; ret.format.type = ResourceFormatType::Regular; ret.showAlpha = false; ret.topology = MakePrimitiveTopology(s.topo, 1); ret.numIndices = s.numVerts; ret.unproject = s.hasPosOut; ret.nearPlane = s.nearPlane; ret.farPlane = s.farPlane; if(instID < s.instData.size()) { VulkanPostVSData::InstData inst = s.instData[instID]; ret.vertexByteOffset = inst.bufOffset; ret.numIndices = inst.numVerts; } return ret; }