blob: 189d8df5eed9c5e4606ef0e528169b8388c65152 [file] [log] [blame]
/* Copyright (c) 2024-2025 The Khronos Group Inc.
* Copyright (c) 2024-2025 Valve Corporation
* Copyright (c) 2024-2025 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 "spirv_logging.h"
#include <string>
#include <cstring>
// Fix GCC 13 issues with regex
#if defined(__GNUC__) && (__GNUC__ > 12)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
#include <regex>
#if defined(__GNUC__) && (__GNUC__ > 12)
#pragma GCC diagnostic pop
#endif
#include "state_tracker/shader_instruction.h"
#include <spirv/unified1/NonSemanticShaderDebugInfo100.h>
#include <spirv/unified1/spirv.hpp>
#include "generated/spirv_grammar_helper.h"
namespace spirv {
static const int kModuleStartingOffset = 5; // first 5 words of module are the headers
static inline uint32_t Opcode(uint32_t instruction) { return instruction & 0x0ffffu; }
static inline uint32_t Length(uint32_t instruction) { return instruction >> 16; }
struct SpirvLoggingInfo {
uint32_t file_string_id = 0; // OpString with filename
uint32_t line_number_start = 0;
uint32_t line_number_end = 0;
// sometimes compiler will just give zero here, so will need to ignore then
uint32_t column_number = 0;
bool using_shader_debug_info = false; // NonSemantic.Shader.DebugInfo.100
std::string reported_filename;
};
// Read the contents of the SPIR-V OpSource instruction and any following continuation instructions.
// Split the single string into a vector of strings, one for each line, for easier processing.
static void ReadOpSource(const std::vector<uint32_t> &instructions, const uint32_t reported_file_id,
std::vector<std::string> &out_source_lines) {
uint32_t offset = kModuleStartingOffset;
while (offset < instructions.size()) {
const uint32_t instruction = instructions[offset];
const uint32_t length = Length(instruction);
const uint32_t opcode = Opcode(instruction);
if (opcode != spv::OpSource || length < 5 || instructions[offset + 3] != reported_file_id) {
offset += length;
continue;
}
// OpSource has been found
std::istringstream in_stream;
std::string current_line;
const char *str = reinterpret_cast<const char *>(&instructions[offset + 4]);
in_stream.str(str);
while (std::getline(in_stream, current_line)) {
out_source_lines.emplace_back(current_line);
}
offset += length; // point to next instruction after OpSource
break;
}
// Look for OpSourceContinued, it must be right after
while (offset < instructions.size()) {
const uint32_t continue_insn = instructions[offset];
const uint32_t length = Length(continue_insn);
const uint32_t opcode = Opcode(continue_insn);
if (opcode != spv::OpSourceContinued) {
break;
}
std::istringstream in_stream;
std::string current_line;
const char *str = reinterpret_cast<const char *>(&instructions[offset + 1]);
in_stream.str(str);
while (std::getline(in_stream, current_line)) {
out_source_lines.emplace_back(current_line);
}
offset += length;
}
}
static void ReadDebugSource(const std::vector<uint32_t> &instructions, const uint32_t debug_source_id, uint32_t &out_file_string_id,
std::vector<std::string> &out_source_lines) {
uint32_t offset = kModuleStartingOffset;
while (offset < instructions.size()) {
const uint32_t instruction = instructions[offset];
const uint32_t length = Length(instruction);
const uint32_t opcode = Opcode(instruction);
if (opcode != spv::OpExtInst || instructions[offset + 2] != debug_source_id ||
instructions[offset + 4] != NonSemanticShaderDebugInfo100DebugSource) {
offset += length;
continue;
}
// We have now found the proper OpExtInst DebugSource
out_file_string_id = instructions[offset + 5];
// Optional source Text not provided so nothing left to do
if (length < 7) {
return;
}
const uint32_t string_id = instructions[offset + 6];
const char *source_text = spirv::GetOpString(instructions, string_id);
if (!source_text) {
return; // error should be caught in spirv-val, but don't crash here
}
std::istringstream in_stream;
std::string current_line;
in_stream.str(source_text);
while (std::getline(in_stream, current_line)) {
out_source_lines.emplace_back(current_line);
}
offset += length; // point to next instruction after OpSource
break;
}
// Look for DebugSourceContinued, it must be right after
while (offset < instructions.size()) {
const uint32_t continue_insn = instructions[offset];
const uint32_t length = Length(continue_insn);
const uint32_t opcode = Opcode(continue_insn);
if (opcode != spv::OpExtInst || instructions[offset + 4] != NonSemanticShaderDebugInfo100DebugSourceContinued) {
break;
}
const uint32_t string_id = instructions[offset + 5];
const char *continue_text = spirv::GetOpString(instructions, string_id);
if (!continue_text) {
return; // error should be caught in spirv-val, but don't crash here
}
std::istringstream in_stream;
std::string current_line;
in_stream.str(continue_text);
while (std::getline(in_stream, current_line)) {
out_source_lines.emplace_back(current_line);
}
offset += length;
}
}
// The task here is to search the OpSource content to find the #line directive with the
// line number that is closest to, but still prior to the reported error line number and
// still within the reported filename.
// From this known position in the OpSource content we can add the difference between
// the #line line number and the reported error line number to determine the location
// in the OpSource content of the reported error line.
//
// Considerations:
// - Look only at #line directives that specify the reported_filename since
// the reported error line number refers to its location in the reported filename.
// - If a #line directive does not have a filename, the file is the reported filename, or
// the filename found in a prior #line directive. (This is C-preprocessor behavior)
// - It is possible (e.g., inlining) for blocks of code to get shuffled out of their
// original order and the #line directives are used to keep the numbering correct. This
// is why we need to examine the entire contents of the source, instead of leaving early
// when finding a #line line number larger than the reported error line number.
//
static bool GetLineFromDirective(const std::string &string, uint32_t *linenumber, std::string &filename) {
static const std::regex line_regex( // matches #line directives
"^" // beginning of line
"\\s*" // optional whitespace
"#" // required text
"\\s*" // optional whitespace
"line" // required text
"\\s+" // required whitespace
"([0-9]+)" // required first capture - line number
"(\\s+)?" // optional second capture - whitespace
"(\".+\")?" // optional third capture - quoted filename with at least one char inside
".*"); // rest of line (needed when using std::regex_match since the entire line is tested)
std::smatch captures;
const bool found_line = std::regex_match(string, captures, line_regex);
if (!found_line) return false;
// filename is optional and considered found only if the whitespace and the filename are captured
if (captures[2].matched && captures[3].matched) {
// Remove enclosing double quotes. The regex guarantees the quotes and at least one char.
filename = captures[3].str().substr(1, captures[3].str().size() - 2);
}
*linenumber = (uint32_t)std::stoul(captures[1]);
return true;
}
// Return false if any error arise
static bool GetLineAndFilename(std::ostringstream &ss, const std::vector<uint32_t> &instructions, SpirvLoggingInfo &logging_info) {
const std::string debug_info_type = (logging_info.using_shader_debug_info) ? "DebugSource" : "OpLine";
if (logging_info.file_string_id == 0) {
// This error should be caught in spirv-val
ss << "(Unable to find file string from SPIR-V " << debug_info_type << ")\n";
return false;
}
const char *file_string_insn = spirv::GetOpString(instructions, logging_info.file_string_id);
if (!file_string_insn) {
// This error should be caught in spirv-val
ss << "(Unable to find SPIR-V OpString " << logging_info.file_string_id << " from " << debug_info_type << " instruction)\n";
return false;
}
// We print out lines the same way gcc/clang does
// somefile.cpp:33:22
// Where 33 is the line and 22 is the column
// Currently we don't have a real good use to try and use the end line/columns (we are not selecting)
logging_info.reported_filename = std::string(file_string_insn);
if (!logging_info.reported_filename.empty()) {
ss << logging_info.reported_filename << ':';
} else {
ss << "<source>:";
}
ss << logging_info.line_number_start;
if (logging_info.column_number != 0) {
ss << ':' << logging_info.column_number;
}
ss << '\n';
return true;
}
static void GetSourceLines(std::ostringstream &ss, const std::vector<std::string> &source_lines,
const SpirvLoggingInfo &logging_info) {
if (source_lines.empty()) {
if (logging_info.using_shader_debug_info) {
ss << "No Text operand found in DebugSource\n";
} else {
ss << "Unable to find SPIR-V OpSource\n";
}
return;
}
// Find the line in the OpSource content that corresponds to the reported error file and line.
uint32_t saved_line_number = 0;
std::string current_filename = logging_info.reported_filename; // current "preprocessor" filename state.
std::vector<std::string>::size_type saved_opsource_offset = 0;
// This was designed to fine the best line if using #line in GLSL
bool found_best_line = false;
if (!logging_info.using_shader_debug_info) {
for (auto it = source_lines.begin(); it != source_lines.end(); ++it) {
uint32_t parsed_line_number;
std::string parsed_filename;
const bool found_line = GetLineFromDirective(*it, &parsed_line_number, parsed_filename);
if (!found_line) continue;
const bool found_filename = parsed_filename.size() > 0;
if (found_filename) {
current_filename = parsed_filename;
}
if ((!found_filename) || (current_filename == logging_info.reported_filename)) {
// Update the candidate best line directive, if the current one is prior and closer to the reported line
if (logging_info.line_number_start >= parsed_line_number) {
if (!found_best_line || (logging_info.line_number_start - parsed_line_number <=
logging_info.line_number_start - saved_line_number)) {
saved_line_number = parsed_line_number;
saved_opsource_offset = std::distance(source_lines.begin(), it);
found_best_line = true;
}
}
}
}
}
if (logging_info.using_shader_debug_info) {
// For Shader Debug Info, we should have all the information we need
ss << '\n';
for (uint32_t line_index = logging_info.line_number_start; line_index <= logging_info.line_number_end; line_index++) {
if (line_index > source_lines.size()) {
ss << line_index << ": [No line found in source]";
break;
}
ss << line_index << ": " << source_lines[line_index - 1] << '\n';
}
// Only show column if since a single line is displayed
if (logging_info.column_number > 0 && logging_info.line_number_start == logging_info.line_number_end) {
// If normally it would be ' someCode' it will look like '23: someCode'
// We need to add columns for the line number prefix we are adding prior
const size_t line_index_spaces = std::to_string(logging_info.line_number_start).length();
const size_t column_count = logging_info.column_number + line_index_spaces + 1;
std::string spaces(column_count, ' ');
ss << spaces << '^';
}
} else if (found_best_line) {
assert(logging_info.line_number_start >= saved_line_number);
const size_t opsource_index = (logging_info.line_number_start - saved_line_number) + 1 + saved_opsource_offset;
if (opsource_index < source_lines.size()) {
ss << '\n' << logging_info.line_number_start << ": " << source_lines[opsource_index] << '\n';
} else {
ss << "Internal error: calculated source line of " << opsource_index << " for source size of " << source_lines.size()
<< " lines\n";
}
} else if (logging_info.line_number_start < source_lines.size() && logging_info.line_number_start != 0) {
// file lines normally start at 1 index
ss << '\n' << source_lines[logging_info.line_number_start - 1] << '\n';
if (logging_info.column_number > 0) {
std::string spaces(logging_info.column_number - 1, ' ');
ss << spaces << '^';
}
} else {
ss << "Unable to find a suitable line in SPIR-V OpSource\n";
}
}
void GetShaderSourceInfo(std::ostringstream &ss, const std::vector<uint32_t> &instructions, const Instruction &last_line_insn) {
// Read the source code and split it up into separate lines.
//
// 1. OpLine will point to a OpSource/OpSourceContinued which have the string built-in
// 2. DebugLine will point to a DebugSource/DebugSourceContinued that each point to a OpString
//
// For the second one, we need to build the source lines up sooner
std::vector<std::string> source_lines;
SpirvLoggingInfo logging_info = {};
if (last_line_insn.Opcode() == spv::OpLine) {
logging_info.using_shader_debug_info = false;
logging_info.file_string_id = last_line_insn.Word(1);
logging_info.line_number_start = last_line_insn.Word(2);
logging_info.line_number_end = logging_info.line_number_start; // OpLine only give a single line granularity
logging_info.column_number = last_line_insn.Word(3);
} else {
// NonSemanticShaderDebugInfo100DebugLine
logging_info.using_shader_debug_info = true;
logging_info.line_number_start = GetConstantValue(instructions, last_line_insn.Word(6));
logging_info.line_number_end = GetConstantValue(instructions, last_line_insn.Word(7));
logging_info.column_number = GetConstantValue(instructions, last_line_insn.Word(8));
const uint32_t debug_source_id = last_line_insn.Word(5);
ReadDebugSource(instructions, debug_source_id, logging_info.file_string_id, source_lines);
}
if (!GetLineAndFilename(ss, instructions, logging_info)) {
return;
}
// Defer finding source from OpLine until we know we have a valid file string to tie it too
if (!logging_info.using_shader_debug_info) {
ReadOpSource(instructions, logging_info.file_string_id, source_lines);
}
GetSourceLines(ss, source_lines, logging_info);
}
const char *GetOpString(const std::vector<uint32_t> &instructions, uint32_t string_id) {
uint32_t offset = kModuleStartingOffset;
while (offset < instructions.size()) {
const uint32_t instruction = instructions[offset];
const uint32_t length = Length(instruction);
const uint32_t opcode = Opcode(instruction);
// if here, seen all OpString and can return early
if (opcode == spv::OpFunction) break;
if (opcode == spv::OpString) {
const uint32_t result_id = instructions[offset + 1];
if (result_id == string_id) {
return reinterpret_cast<const char *>(&instructions[offset + 2]);
}
}
offset += length;
}
return nullptr;
}
uint32_t GetConstantValue(const std::vector<uint32_t> &instructions, uint32_t constant_id) {
uint32_t offset = kModuleStartingOffset;
while (offset < instructions.size()) {
const uint32_t instruction = instructions[offset];
const uint32_t length = Length(instruction);
const uint32_t opcode = Opcode(instruction);
// if here, seen all OpConstant and can return early
if (opcode == spv::OpFunction) break;
if (opcode == spv::OpConstant) {
const uint32_t result_id = instructions[offset + 2];
if (result_id == constant_id) {
return instructions[offset + 3];
}
}
offset += length;
}
assert(false);
return 0;
}
void GetExecutionModelNames(const std::vector<uint32_t> &instructions, std::ostringstream &ss) {
bool first_stage = true;
uint32_t offset = kModuleStartingOffset;
while (offset < instructions.size()) {
const uint32_t instruction = instructions[offset];
const uint32_t length = Length(instruction);
const uint32_t opcode = Opcode(instruction);
// if here, seen all OpEntryPoint and can return early
if (opcode == spv::OpFunction) break;
if (opcode == spv::OpEntryPoint) {
if (first_stage) {
first_stage = false;
} else {
ss << ", ";
}
const uint32_t entry_point_id = instructions[offset + 1];
ss << string_SpvExecutionModel(entry_point_id);
}
offset += length;
}
}
// Find the OpLine/DebugLine just before the failing instruction indicated by the debug info.
// Return the offset into the instructions array
static uint32_t GetDebugLineOffset(const std::vector<uint32_t> &instructions, uint32_t instruction_position_offset) {
uint32_t shader_debug_info_set_id = 0;
uint32_t last_line_inst_offset = 0;
uint32_t offset = kModuleStartingOffset;
while (offset < instructions.size()) {
const uint32_t instruction = instructions[offset];
const uint32_t length = Length(instruction);
const uint32_t opcode = Opcode(instruction);
if (opcode == spv::OpExtInstImport) {
const char *str = reinterpret_cast<const char *>(&instructions[offset + 2]);
if (strcmp(str, "NonSemantic.Shader.DebugInfo.100") == 0) {
shader_debug_info_set_id = instructions[offset + 1];
}
}
if (opcode == spv::OpExtInst && instructions[offset + 3] == shader_debug_info_set_id &&
instructions[offset + 4] == NonSemanticShaderDebugInfo100DebugLine) {
last_line_inst_offset = offset;
} else if (opcode == spv::OpLine) {
last_line_inst_offset = offset;
} else if (opcode == spv::OpFunctionEnd) {
last_line_inst_offset = 0; // debug lines can't cross functions boundaries
}
offset += length;
if (offset >= instruction_position_offset) {
break;
}
}
return last_line_inst_offset;
}
// There are 2 ways to inject source into a shader:
// 1. The "old" way using OpLine/OpSource
// 2. The "new" way using NonSemantic Shader DebugInfo
void FindShaderSource(std::ostringstream &ss, const std::vector<uint32_t> &instructions, uint32_t instruction_position_offset,
bool debug_printf_only) {
const uint32_t last_line_offset = GetDebugLineOffset(instructions, instruction_position_offset);
if (last_line_offset != 0) {
Instruction last_line_inst(instructions.data() + last_line_offset);
ss << (debug_printf_only ? "Debug shader printf message generated at " : "Shader validation error occurred at ");
GetShaderSourceInfo(ss, instructions, last_line_inst);
} else if (instruction_position_offset != 0) {
spirv::Instruction target_inst(instructions.data() + instruction_position_offset);
ss << "SPIR-V Instruction: " << target_inst.Describe()
<< "\n(Unable to find shader source, build shader with debug info to get source information)\n";
} else {
ss << "(This check was instrumented at the start of your entrypoint function)\n";
}
}
} // namespace spirv