blob: 835d2d94725efa8a8d8ea17235f6935b07e92002 [file] [log] [blame]
/* Copyright (c) 2020-2026 The Khronos Group Inc.
* Copyright (c) 2020-2026 Valve Corporation
* Copyright (c) 2020-2026 LunarG, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <vulkan/vulkan.h>
#include "gpuav/debug_printf/debug_printf.h"
#include "gpuav/resources/gpuav_shader_resources.h"
#include "gpuav/shaders/gpuav_error_header.h"
#include "gpuav/shaders/gpuav_shaders_constants.h"
#include "gpuav/resources/gpuav_state_trackers.h"
#include "gpuav/core/gpuav.h"
#include "error_message/spirv_logging.h"
#include <iostream>
namespace gpuav {
namespace debug_printf {
enum NumericType {
NumericTypeUnknown = 0,
NumericTypeFloat = 1,
NumericTypeSint = 2,
NumericTypeUint = 4,
};
static NumericType NumericTypeLookup(char specifier) {
switch (specifier) {
case 'd':
case 'i':
return NumericTypeSint;
break;
case 'f':
case 'F':
case 'a':
case 'A':
case 'e':
case 'E':
case 'g':
case 'G':
return NumericTypeFloat;
break;
case 'u':
case 'x':
case 'o':
default:
return NumericTypeUint;
break;
}
}
struct Substring {
std::string string;
bool needs_value = false; // if value from buffer needed to print arguments
NumericType type = NumericTypeUnknown;
bool is_64_bit = false;
bool is_pointer = false;
};
static std::vector<Substring> ParseFormatString(const std::string &format_string) {
const char types[] = {'d', 'i', 'o', 'u', 'x', 'X', 'a', 'A', 'e', 'E', 'f', 'F', 'g', 'G', 'v', 'p', '\0'};
std::vector<Substring> parsed_strings;
size_t pos = 0;
size_t begin = 0;
size_t percent = 0;
while (begin < format_string.length()) {
Substring substring;
// Find a percent sign
pos = percent = format_string.find_first_of('%', pos);
if (pos == std::string::npos) {
// End of the format string Push the rest of the characters
substring.string = format_string.substr(begin, format_string.length());
parsed_strings.emplace_back(substring);
break;
}
pos++;
if (format_string[pos] == '%') {
pos++;
continue; // %% - skip it
}
// Find the type of the value
pos = format_string.find_first_of(types, pos);
if (pos == format_string.npos) {
// This really shouldn't happen with a legal value string
pos = format_string.length();
} else {
substring.needs_value = true;
// We are just taking vector and creating a list of scalar that snprintf can handle
if (format_string[pos] == 'v') {
// Vector must be of size 2, 3, or 4
// and format %v<size><type>
std::string specifier = format_string.substr(percent, pos - percent);
const int vec_size = atoi(&format_string[pos + 1]);
pos += 2;
// skip v<count>, handle long
specifier.push_back(format_string[pos]);
if (format_string[pos + 1] == 'l') {
// catches %ul
substring.is_64_bit = true;
specifier.push_back('l');
pos++;
} else if (format_string[pos] == 'l') {
// catches %lu and lx
substring.is_64_bit = true;
specifier.push_back(format_string[pos + 1]);
pos++;
}
// Take the preceding characters, and the percent through the type
substring.string = format_string.substr(begin, percent - begin);
substring.string += specifier;
substring.type = NumericTypeLookup(specifier.back());
parsed_strings.emplace_back(substring);
// Continue with a comma separated list
char temp_string[32];
snprintf(temp_string, sizeof(temp_string), ", %s", specifier.c_str());
substring.string = temp_string;
for (int i = 0; i < (vec_size - 1); i++) {
parsed_strings.emplace_back(substring);
}
} else {
// Single non-vector value
if (format_string[pos - 1] == 'l') {
substring.is_64_bit = true; // finds %lu since we skipped the 'l' to find the 'u'
} else if (format_string[pos + 1] == 'l') {
substring.is_64_bit = true;
pos++; // Save long size
}
if (format_string[pos] == 'p') {
substring.is_64_bit = true;
substring.is_pointer = true;
}
substring.string = format_string.substr(begin, pos - begin + 1);
substring.type = NumericTypeLookup(format_string[pos]);
parsed_strings.emplace_back(substring);
}
begin = pos + 1;
}
}
return parsed_strings;
}
// GCC and clang don't like using variables as format strings in sprintf.
// #pragma GCC is recognized by both compilers
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-security"
#endif
// The contents each "printf" is writting to the output buffer streams
struct OutputRecord {
uint32_t size;
uint32_t shader_id;
uint32_t instruction_position_offset;
uint32_t format_string_id;
uint32_t double_bitmask; // used to distinguish if float is 1 or 2 dwords
uint32_t signed_8_bitmask; // used to distinguish if signed int is a int8_t
uint32_t signed_16_bitmask; // used to distinguish if signed int is a int16_t
uint32_t stage_id;
uint32_t stage_info_0;
uint32_t stage_info_1;
uint32_t stage_info_2;
uint32_t values; // place holder to be casted to get rest of items in record
};
struct DebugPrintfBufferInfo {
// The buffer where DebugPrintf data was written to (and need to report)
vko::BufferRange output_mem_buffer;
// Same as GPU-AV, we need these to generate details in error message where error occured in the CmdBuffer
VkPipelineBindPoint pipeline_bind_point;
uint32_t action_command_index;
// Before the draw/dispatch/etc we can save the pipeline/shaderObject that are being used
LogObjectList objlist;
DebugPrintfBufferInfo(vko::BufferRange output_mem_buffer, VkPipelineBindPoint pipeline_bind_point,
uint32_t action_command_index, const LogObjectList &objlist)
: output_mem_buffer(output_mem_buffer),
pipeline_bind_point(pipeline_bind_point),
action_command_index(action_command_index),
objlist(objlist){};
};
struct DebugPrintfCbState {
std::vector<DebugPrintfBufferInfo> buffer_infos;
};
void AnalyzeAndGenerateMessage(Validator &gpuav, VkCommandBuffer command_buffer, DebugPrintfBufferInfo &buffer_info,
uint32_t *const debug_output_buffer, const Location &loc) {
uint32_t output_buffer_dwords_counts = debug_output_buffer[gpuav::kDebugPrintf_OutputBuffer_DWordsCount];
if (!output_buffer_dwords_counts) return;
uint32_t output_record_i = gpuav::kDebugPrintf_OutputBuffer_Data; // get first OutputRecord index
while (debug_output_buffer[output_record_i]) {
std::ostringstream shader_message;
OutputRecord *debug_record = reinterpret_cast<OutputRecord *>(&debug_output_buffer[output_record_i]);
// Lookup the VkShaderModule handle and SPIR-V code used to create the shader, using the unique shader ID value returned
// by the instrumented shader.
const gpuav::InstrumentedShader *instrumented_shader = nullptr;
auto it = gpuav.instrumented_shaders_map_.find(debug_record->shader_id);
if (it != gpuav.instrumented_shaders_map_.end()) {
instrumented_shader = &it->second;
}
// without the instrumented spirv, there is nothing valuable to print out
if (!instrumented_shader || instrumented_shader->original_spirv.empty()) {
gpuav.InternalWarning(buffer_info.objlist, loc, "Can't find instructions from any handles in shader_map");
return;
}
// Search through the shader source for the printf format string for this invocation
std::string format_string;
const char *op_string = ::spirv::GetOpString(instrumented_shader->original_spirv, debug_record->format_string_id);
if (op_string) {
format_string = std::string(op_string);
} else {
// We have plumbed the OpString from the instrumented shader
for (auto debug_instrumented_info : gpuav.internal_only_debug_printf_) {
if ((debug_instrumented_info.unique_shader_id == debug_record->shader_id) &&
(debug_record->format_string_id == debug_instrumented_info.op_string_id)) {
format_string = debug_instrumented_info.op_string_text;
break;
}
}
}
// Break the format string into strings with 1 or 0 value
auto format_substrings = ParseFormatString(format_string);
void *current_value = static_cast<void *>(&debug_record->values);
// Sprintf each format substring into a temporary string then add that to the message
for (size_t substring_i = 0; substring_i < format_substrings.size(); substring_i++) {
auto &substring = format_substrings[substring_i];
std::string temp_string;
size_t needed = 0;
if (substring.needs_value) {
if (substring.is_64_bit) {
if (substring.type == NumericTypeUint) {
std::array<std::string_view, 3> format_strings = {{"%ul", "%lu", "%lx"}};
for (const auto &ul_string : format_strings) {
size_t ul_pos = substring.string.find(ul_string);
if (ul_pos == std::string::npos) continue;
if (ul_string != "%lu") {
substring.string.replace(ul_pos + 1, 2, PRIx64);
} else {
substring.string.replace(ul_pos + 1, 2, PRIu64);
}
break;
}
const uint64_t value = *static_cast<uint64_t *>(current_value);
// +1 for null terminator
needed = std::snprintf(nullptr, 0, substring.string.c_str(), value) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str(), value);
} else if (substring.type == NumericTypeSint) {
size_t ld_pos = substring.string.find("%ld");
if (ld_pos != std::string::npos) {
substring.string.replace(ld_pos + 1, 2, PRId64);
} else {
gpuav.InternalWarning(buffer_info.objlist, loc,
"Trying to DebugPrintf a 64-bit signed int but not using \"%%ld\" to print it.");
}
const uint32_t *current_ptr = static_cast<uint32_t *>(current_value);
const uint64_t value_unsigned = glsl::GetUint64(current_ptr);
const int64_t value = static_cast<int64_t>(value_unsigned);
needed = std::snprintf(nullptr, 0, substring.string.c_str(), value) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str(), value);
} else {
assert(false); // non-supported type
}
} else {
if (substring.type == NumericTypeUint) {
// +1 for null terminator
const uint32_t value = *static_cast<uint32_t *>(current_value);
needed = std::snprintf(nullptr, 0, substring.string.c_str(), value) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str(), value);
} else if (substring.type == NumericTypeSint) {
// When dealing with signed int, we need to know which size the int was to print the correct value
if (debug_record->signed_8_bitmask & (1 << substring_i)) {
const int8_t value = *static_cast<int8_t *>(current_value);
needed = std::snprintf(nullptr, 0, substring.string.c_str(), value) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str(), value);
} else if (debug_record->signed_16_bitmask & (1 << substring_i)) {
const int16_t value = *static_cast<int16_t *>(current_value);
needed = std::snprintf(nullptr, 0, substring.string.c_str(), value) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str(), value);
} else {
const int32_t value = *static_cast<int32_t *>(current_value);
needed = std::snprintf(nullptr, 0, substring.string.c_str(), value) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str(), value);
}
} else if (substring.type == NumericTypeFloat) {
// On the CPU printf the "%f" is used for 16, 32, and 64-bit floats,
// but we need to store the 64-bit floats in 2 dwords in our GPU side buffer.
// Using the bitmask, we know if the incoming float was 64-bit or not.
// This is much simpler than enforcing a %lf which doesn't line up with how the CPU side works
if (debug_record->double_bitmask & (1 << substring_i)) {
substring.is_64_bit = true;
const double value = *static_cast<double *>(current_value);
needed = std::snprintf(nullptr, 0, substring.string.c_str(), value) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str(), value);
} else {
const float value = *static_cast<float *>(current_value);
needed = std::snprintf(nullptr, 0, substring.string.c_str(), value) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str(), value);
}
}
}
const uint32_t offset = substring.is_64_bit ? 2 : 1;
current_value = static_cast<uint32_t *>(current_value) + offset;
} else {
// incase where someone just printing a string with no arguments to it
needed = std::snprintf(nullptr, 0, substring.string.c_str()) + 1;
temp_string.resize(needed);
std::snprintf(&temp_string[0], needed, substring.string.c_str());
}
shader_message << temp_string.c_str();
}
const bool use_stdout = gpuav.gpuav_settings.debug_printf_to_stdout;
if (gpuav.gpuav_settings.debug_printf_verbose) {
GpuShaderInstrumentor::ShaderMessageInfo shader_info{debug_record->stage_id,
debug_record->stage_info_0,
debug_record->stage_info_1,
debug_record->stage_info_2,
debug_record->instruction_position_offset,
debug_record->shader_id};
std::string debug_info_message =
gpuav.GenerateDebugInfoMessage(command_buffer, shader_info, instrumented_shader, buffer_info.pipeline_bind_point,
buffer_info.action_command_index);
if (use_stdout) {
std::cout << "VVL-DEBUG-PRINTF " << shader_message.str() << '\n' << debug_info_message;
} else {
gpuav.LogInfo("VVL-DEBUG-PRINTF", buffer_info.objlist, loc, "DebugPrintf:\n%s\n%s", shader_message.str().c_str(),
debug_info_message.c_str());
}
} else {
if (use_stdout) {
std::cout << shader_message.str();
} else {
gpuav.LogInfo("VVL-DEBUG-PRINTF", buffer_info.objlist, loc, "DebugPrintf:\n%s", shader_message.str().c_str());
}
}
output_record_i += debug_record->size;
}
if ((output_record_i - gpuav::kDebugPrintf_OutputBuffer_Data) < output_buffer_dwords_counts) {
// Originally we had this to log a warning, but if using the default settings, warnings are hidden.
// We report this information the same we report the "real" debug printf message so we know it is seen
std::ostringstream message;
message << "[WARNING] Debug Printf message was truncated due to the buffer size ("
<< gpuav.gpuav_settings.debug_printf_buffer_size << ") being too small for the messages consuming "
<< output_buffer_dwords_counts
<< " bytes.\nThis can be adjusted setting env var VK_LAYER_PRINTF_BUFFER_SIZE=" << output_buffer_dwords_counts
<< " or in vkconfig";
if (gpuav.gpuav_settings.debug_printf_to_stdout) {
std::cout << message.str() << "\n";
} else {
gpuav.LogInfo("VVL-DEBUG-PRINTF", buffer_info.objlist, loc, "%s", message.str().c_str());
}
}
// Only memset what is needed, in case we are only using a small portion of a large buffer_size.
// At the same time we want to make sure we don't memset past the actual VkBuffer allocation
uint32_t clear_size = sizeof(uint32_t) * (debug_output_buffer[gpuav::kDebugPrintf_OutputBuffer_DWordsCount] +
gpuav::kDebugPrintf_OutputBuffer_Data);
clear_size = std::min(gpuav.gpuav_settings.debug_printf_buffer_size, clear_size);
memset(debug_output_buffer, 0, clear_size);
}
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
void RegisterDebugPrintf(Validator &gpuav, CommandBufferSubState &cb_state) {
if (!gpuav.gpuav_settings.debug_printf_enabled) {
return;
}
cb_state.on_instrumentation_desc_set_update_functions.emplace_back(
[debug_printf_buffer_size = gpuav.gpuav_settings.debug_printf_buffer_size](
CommandBufferSubState &cb, VkPipelineBindPoint bind_point, const Location &, VkDescriptorBufferInfo &out_buffer_info,
uint32_t &out_dst_binding) {
vko::BufferRange debug_printf_output_buffer =
cb.gpu_resources_manager.GetHostCoherentBufferRange(debug_printf_buffer_size);
std::memset(debug_printf_output_buffer.offset_mapped_ptr, 0, (size_t)debug_printf_buffer_size);
out_buffer_info.buffer = debug_printf_output_buffer.buffer;
out_buffer_info.offset = debug_printf_output_buffer.offset;
out_buffer_info.range = debug_printf_output_buffer.size;
out_dst_binding = glsl::kBindingInstDebugPrintf;
DebugPrintfCbState &debug_printf_cb_state = cb.shared_resources_cache.GetOrCreate<DebugPrintfCbState>();
debug_printf_cb_state.buffer_infos.emplace_back(
debug_printf_output_buffer, bind_point, cb.GetActionCommandIndex(bind_point), cb.base.GetObjectList(bind_point));
});
cb_state.on_instrumentation_desc_buffer_update_functions.emplace_back(
[debug_printf_buffer_size = gpuav.gpuav_settings.debug_printf_buffer_size](
CommandBufferSubState &cb, VkPipelineBindPoint bind_point, VkDescriptorAddressInfoEXT &out_address_info,
uint32_t &out_dst_binding) {
vko::BufferRange debug_printf_output_buffer =
cb.gpu_resources_manager.GetHostCoherentBufferRange(debug_printf_buffer_size);
std::memset(debug_printf_output_buffer.offset_mapped_ptr, 0, (size_t)debug_printf_buffer_size);
out_address_info.address = debug_printf_output_buffer.offset_address;
out_address_info.range = debug_printf_output_buffer.size;
out_dst_binding = glsl::kBindingInstDebugPrintf;
DebugPrintfCbState &debug_printf_cb_state = cb.shared_resources_cache.GetOrCreate<DebugPrintfCbState>();
debug_printf_cb_state.buffer_infos.emplace_back(
debug_printf_output_buffer, bind_point, cb.GetActionCommandIndex(bind_point), cb.base.GetObjectList(bind_point));
});
cb_state.on_instrumentation_desc_heap_update_functions.emplace_back(
[debug_printf_buffer_size = gpuav.gpuav_settings.debug_printf_buffer_size](
CommandBufferSubState &cb, VkPipelineBindPoint bind_point, VkDeviceAddress &out_address) {
vko::BufferRange debug_printf_output_buffer =
cb.gpu_resources_manager.GetHostCoherentBufferRange(debug_printf_buffer_size);
std::memset(debug_printf_output_buffer.offset_mapped_ptr, 0, (size_t)debug_printf_buffer_size);
out_address = debug_printf_output_buffer.offset_address;
DebugPrintfCbState &debug_printf_cb_state = cb.shared_resources_cache.GetOrCreate<DebugPrintfCbState>();
debug_printf_cb_state.buffer_infos.emplace_back(
debug_printf_output_buffer, bind_point, cb.GetActionCommandIndex(bind_point), cb.base.GetObjectList(bind_point));
});
cb_state.on_cb_completion_functions.emplace_back([](Validator &gpuav, CommandBufferSubState &cb,
const CommandBufferSubState::LabelLogging &label_logging,
const Location &loc) {
DebugPrintfCbState *debug_printf_cb_state = cb.shared_resources_cache.TryGet<DebugPrintfCbState>();
if (!debug_printf_cb_state) {
return true;
}
for (DebugPrintfBufferInfo &printf_buffer_info : debug_printf_cb_state->buffer_infos) {
auto printf_output_ptr = (char *)printf_buffer_info.output_mem_buffer.offset_mapped_ptr;
debug_printf::AnalyzeAndGenerateMessage(gpuav, cb.VkHandle(), printf_buffer_info, (uint32_t *)printf_output_ptr, loc);
}
return true;
});
}
} // namespace debug_printf
} // namespace gpuav