// Copyright 2018 The Amber Authors. // // 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 "src/verifier.h" #include #include #include #include #include #include "src/command.h" namespace amber { namespace { const uint8_t kBitsPerByte = 8; const double kEpsilon = 0.000001; const double kDefaultTexelTolerance = 0.002; // Copy [src_bit_offset, src_bit_offset + bits) bits of |src| to // [0, bits) of |dst|. void CopyBitsOfMemoryToBuffer(uint8_t* dst, const uint8_t* src, uint8_t src_bit_offset, uint8_t bits) { while (src_bit_offset > static_cast(7)) { ++src; src_bit_offset = static_cast(src_bit_offset - kBitsPerByte); } // Number of bytes greater than or equal to |(src_bit_offset + bits) / 8|. const uint8_t size_in_bytes = static_cast((src_bit_offset + bits + 7) / kBitsPerByte); assert(size_in_bytes <= static_cast(kBitsPerByte)); uint64_t data = 0; uint8_t* ptr = reinterpret_cast(&data); for (uint8_t i = 0; i < size_in_bytes; ++i) { ptr[i] = src[i]; } data >>= src_bit_offset; if (bits != 64) data &= (1ULL << bits) - 1ULL; std::memcpy(dst, &data, static_cast((bits + 7) / kBitsPerByte)); } // Convert float |value| whose size is 16 bits to 32 bits float // based on IEEE-754. float HexFloat16ToFloat(const uint8_t* value) { uint32_t sign = (static_cast(value[1]) & 0x80) << 24U; uint32_t exponent = (((static_cast(value[1]) & 0x7c) >> 2U) + 112U) << 23U; uint32_t mantissa = ((static_cast(value[1]) & 0x3) << 8U | static_cast(value[0])) << 13U; uint32_t hex = sign | exponent | mantissa; float* hex_float = reinterpret_cast(&hex); return *hex_float; } // Convert float |value| whose size is 11 bits to 32 bits float // based on IEEE-754. float HexFloat11ToFloat(const uint8_t* value) { uint32_t exponent = (((static_cast(value[1]) << 2U) | ((static_cast(value[0]) & 0xc0) >> 6U)) + 112U) << 23U; uint32_t mantissa = (static_cast(value[0]) & 0x3f) << 17U; uint32_t hex = exponent | mantissa; float* hex_float = reinterpret_cast(&hex); return *hex_float; } // Convert float |value| whose size is 10 bits to 32 bits float // based on IEEE-754. float HexFloat10ToFloat(const uint8_t* value) { uint32_t exponent = (((static_cast(value[1]) << 3U) | ((static_cast(value[0]) & 0xe0) >> 5U)) + 112U) << 23U; uint32_t mantissa = (static_cast(value[0]) & 0x1f) << 18U; uint32_t hex = exponent | mantissa; float* hex_float = reinterpret_cast(&hex); return *hex_float; } // Convert float |value| whose size is |bits| bits to 32 bits float // based on IEEE-754. // See https://www.khronos.org/opengl/wiki/Small_Float_Formats // and https://en.wikipedia.org/wiki/IEEE_754. // // Sign Exponent Mantissa Exponent-Bias // 16 1 5 10 15 // 11 0 5 6 15 // 10 0 5 5 15 // 32 1 8 23 127 // 64 1 11 52 1023 // // 11 and 10 bits floats are always positive. // 14 bits float is used only RGB9_E5 format in OpenGL but it does not exist // in Vulkan. float HexFloatToFloat(const uint8_t* value, uint8_t bits) { switch (bits) { case 10: return HexFloat10ToFloat(value); case 11: return HexFloat11ToFloat(value); case 16: return HexFloat16ToFloat(value); } assert(false && "Invalid bits"); return 0; } // This is based on "18.3. sRGB transfer functions" of // https://www.khronos.org/registry/DataFormat/specs/1.2/dataformat.1.2.html double SRGBToLinearValue(double sRGB) { if (sRGB <= 0.04045) return sRGB / 12.92; return pow((sRGB + 0.055) / 1.055, 2.4); } // It returns true if the difference is within the given error. // If |is_tolerance_percent| is true, the actual tolerance will be // relative value i.e., |tolerance| / 100 * fabs(expected). // Otherwise, this method uses the absolute value i.e., |tolerance|. bool IsEqualWithTolerance(const double actual, const double expected, double tolerance, const bool is_tolerance_percent = true) { double difference = std::fabs(actual - expected); if (is_tolerance_percent) { if (difference > ((tolerance / 100.0) * std::fabs(expected))) { return false; } } else if (difference > tolerance) { return false; } return true; } template Result CheckValue(const ProbeSSBOCommand* command, const uint8_t* memory, const std::vector& values) { const auto comp = command->GetComparator(); const auto& tolerance = command->GetTolerances(); const T* ptr = reinterpret_cast(memory); for (size_t i = 0; i < values.size(); ++i) { const T val = values[i].IsInteger() ? static_cast(values[i].AsUint64()) : static_cast(values[i].AsDouble()); switch (comp) { case ProbeSSBOCommand::Comparator::kEqual: if (values[i].IsInteger()) { if (static_cast(*ptr) != static_cast(val)) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " == " + std::to_string(val) + ", at index " + std::to_string(i)); } } else { if (!IsEqualWithTolerance(static_cast(*ptr), static_cast(val), kEpsilon)) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " == " + std::to_string(val) + ", at index " + std::to_string(i)); } } break; case ProbeSSBOCommand::Comparator::kNotEqual: if (values[i].IsInteger()) { if (static_cast(*ptr) == static_cast(val)) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " != " + std::to_string(val) + ", at index " + std::to_string(i)); } } else { if (IsEqualWithTolerance(static_cast(*ptr), static_cast(val), kEpsilon)) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " != " + std::to_string(val) + ", at index " + std::to_string(i)); } } break; case ProbeSSBOCommand::Comparator::kFuzzyEqual: if (!IsEqualWithTolerance( static_cast(*ptr), static_cast(val), command->HasTolerances() ? tolerance[0].value : kEpsilon, command->HasTolerances() ? tolerance[0].is_percent : true)) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " ~= " + std::to_string(val) + ", at index " + std::to_string(i)); } break; case ProbeSSBOCommand::Comparator::kLess: if (*ptr >= val) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " < " + std::to_string(val) + ", at index " + std::to_string(i)); } break; case ProbeSSBOCommand::Comparator::kLessOrEqual: if (*ptr > val) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " <= " + std::to_string(val) + ", at index " + std::to_string(i)); } break; case ProbeSSBOCommand::Comparator::kGreater: if (*ptr <= val) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " > " + std::to_string(val) + ", at index " + std::to_string(i)); } break; case ProbeSSBOCommand::Comparator::kGreaterOrEqual: if (*ptr < val) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier failed: " + std::to_string(*ptr) + " >= " + std::to_string(val) + ", at index " + std::to_string(i)); } break; } ++ptr; } return {}; } void SetupToleranceForTexels(const ProbeCommand* command, double* tolerance, bool* is_tolerance_percent) { if (command->HasTolerances()) { const auto& tol = command->GetTolerances(); if (tol.size() == 4) { tolerance[0] = tol[0].value; tolerance[1] = tol[1].value; tolerance[2] = tol[2].value; tolerance[3] = tol[3].value; is_tolerance_percent[0] = tol[0].is_percent; is_tolerance_percent[1] = tol[1].is_percent; is_tolerance_percent[2] = tol[2].is_percent; is_tolerance_percent[3] = tol[3].is_percent; } else { tolerance[0] = tol[0].value; tolerance[1] = tol[0].value; tolerance[2] = tol[0].value; tolerance[3] = tol[0].value; is_tolerance_percent[0] = tol[0].is_percent; is_tolerance_percent[1] = tol[0].is_percent; is_tolerance_percent[2] = tol[0].is_percent; is_tolerance_percent[3] = tol[0].is_percent; } } else { tolerance[0] = kDefaultTexelTolerance; tolerance[1] = kDefaultTexelTolerance; tolerance[2] = kDefaultTexelTolerance; tolerance[3] = kDefaultTexelTolerance; is_tolerance_percent[0] = false; is_tolerance_percent[1] = false; is_tolerance_percent[2] = false; is_tolerance_percent[3] = false; } } // Convert data of |texel| into double values based on the // information given in |framebuffer_format|. std::vector GetActualValuesFromTexel(const uint8_t* texel, const Format* framebuffer_format) { assert(framebuffer_format && !framebuffer_format->GetComponents().empty()); std::vector actual_values(framebuffer_format->GetComponents().size()); uint8_t bit_offset = 0; for (size_t i = 0; i < framebuffer_format->GetComponents().size(); ++i) { const auto& component = framebuffer_format->GetComponents()[i]; uint8_t actual[8] = {0, 0, 0, 0, 0, 0, 0, 0}; CopyBitsOfMemoryToBuffer(actual, texel, bit_offset, component.num_bits); if (component.mode == FormatMode::kUFloat || component.mode == FormatMode::kSFloat) { if (component.num_bits < 32) { actual_values[i] = static_cast(HexFloatToFloat(actual, component.num_bits)); } else if (component.num_bits == 32) { float* ptr = reinterpret_cast(actual); actual_values[i] = static_cast(*ptr); } else if (component.num_bits == 64) { double* ptr = reinterpret_cast(actual); actual_values[i] = *ptr; } else { assert(false && "Bits of component is not for double nor float type"); } } else { if (component.mode == FormatMode::kSInt || component.mode == FormatMode::kSNorm) { switch (component.num_bits) { case 8: { int8_t* ptr8 = nullptr; ptr8 = reinterpret_cast(actual); actual_values[i] = static_cast(*ptr8); break; } case 16: { int16_t* ptr16 = nullptr; ptr16 = reinterpret_cast(actual); actual_values[i] = static_cast(*ptr16); break; } case 32: { int32_t* ptr32 = nullptr; ptr32 = reinterpret_cast(actual); actual_values[i] = static_cast(*ptr32); break; } case 64: { int64_t* ptr64 = nullptr; ptr64 = reinterpret_cast(actual); actual_values[i] = static_cast(*ptr64); break; } default: { assert(false && "Bits of component is not for integer type"); } } } else { switch (component.num_bits) { case 8: { actual_values[i] = static_cast(*actual); break; } case 16: { uint16_t* ptr16 = nullptr; ptr16 = reinterpret_cast(actual); actual_values[i] = static_cast(*ptr16); break; } case 32: { uint32_t* ptr32 = nullptr; ptr32 = reinterpret_cast(actual); actual_values[i] = static_cast(*ptr32); break; } case 64: { uint64_t* ptr64 = nullptr; ptr64 = reinterpret_cast(actual); actual_values[i] = static_cast(*ptr64); break; } default: { assert(false && "Bits of component is not for integer type"); } } } } bit_offset = static_cast(bit_offset + component.num_bits); } return actual_values; } // If component mode of |framebuffer_format| is FormatMode::kUNorm or // ::kSNorm or ::kSRGB, scale the corresponding value in |texel|. // Note that we do not scale values with FormatMode::kUInt, ::kSInt, // ::kUFloat, ::kSFloat. void ScaleTexelValuesIfNeeded(std::vector* texel, const Format* framebuffer_format) { assert(framebuffer_format->GetComponents().size() == texel->size()); for (size_t i = 0; i < framebuffer_format->GetComponents().size(); ++i) { const auto& component = framebuffer_format->GetComponents()[i]; double scaled_value = (*texel)[i]; switch (component.mode) { case FormatMode::kUNorm: scaled_value /= static_cast((1 << component.num_bits) - 1); break; case FormatMode::kSNorm: scaled_value /= static_cast((1 << (component.num_bits - 1)) - 1); break; case FormatMode::kUInt: case FormatMode::kSInt: case FormatMode::kUFloat: case FormatMode::kSFloat: break; case FormatMode::kSRGB: scaled_value /= static_cast((1 << component.num_bits) - 1); if (component.type != FormatComponentType::kA) scaled_value = SRGBToLinearValue(scaled_value); break; case FormatMode::kUScaled: case FormatMode::kSScaled: assert(false && "FormatMode::kUScaled and ::kSScaled are not implemented"); break; } (*texel)[i] = scaled_value; } } /// Check |texel| with |texel_format| is the same with the expected /// RGB(A) values given via |command|. This method allow error /// smaller than |tolerance|. If an element of /// |is_tolerance_percent| is true, we assume that the corresponding /// |tolerance| is relative i.e., percentage allowed error. bool IsTexelEqualToExpected(const std::vector& texel, const Format* framebuffer_format, const ProbeCommand* command, const double* tolerance, const bool* is_tolerance_percent) { for (size_t i = 0; i < framebuffer_format->GetComponents().size(); ++i) { const auto& component = framebuffer_format->GetComponents()[i]; double texel_for_component = texel[i]; double expected = 0; double current_tolerance = 0; bool is_current_tolerance_percent = false; switch (component.type) { case FormatComponentType::kA: if (!command->IsRGBA()) { continue; } expected = static_cast(command->GetA()); current_tolerance = tolerance[3]; is_current_tolerance_percent = is_tolerance_percent[3]; break; case FormatComponentType::kR: expected = static_cast(command->GetR()); current_tolerance = tolerance[0]; is_current_tolerance_percent = is_tolerance_percent[0]; break; case FormatComponentType::kG: expected = static_cast(command->GetG()); current_tolerance = tolerance[1]; is_current_tolerance_percent = is_tolerance_percent[1]; break; case FormatComponentType::kB: expected = static_cast(command->GetB()); current_tolerance = tolerance[2]; is_current_tolerance_percent = is_tolerance_percent[2]; break; default: continue; } if (!IsEqualWithTolerance(expected, texel_for_component, current_tolerance, is_current_tolerance_percent)) { return false; } } return true; } std::vector GetTexelInRGBA(const std::vector& texel, const Format* framebuffer_format) { std::vector texel_in_rgba(texel.size()); for (size_t i = 0; i < framebuffer_format->GetComponents().size(); ++i) { const auto& component = framebuffer_format->GetComponents()[i]; switch (component.type) { case FormatComponentType::kR: texel_in_rgba[0] = texel[i]; break; case FormatComponentType::kG: texel_in_rgba[1] = texel[i]; break; case FormatComponentType::kB: texel_in_rgba[2] = texel[i]; break; case FormatComponentType::kA: texel_in_rgba[3] = texel[i]; break; default: continue; } } return texel_in_rgba; } } // namespace Verifier::Verifier() = default; Verifier::~Verifier() = default; Result Verifier::Probe(const ProbeCommand* command, const Format* framebuffer_format, uint32_t texel_stride, uint32_t row_stride, uint32_t frame_width, uint32_t frame_height, const void* buf) { if (!command) return Result("Verifier::Probe given ProbeCommand is nullptr"); if (!framebuffer_format) return Result("Verifier::Probe given texel's Format is nullptr"); if (!buf) return Result("Verifier::Probe given buffer to probe is nullptr"); uint32_t x = 0; uint32_t y = 0; uint32_t width = 1; uint32_t height = 1; if (command->IsWholeWindow()) { width = frame_width; height = frame_height; } else if (command->IsRelative()) { x = static_cast(static_cast(frame_width) * command->GetX()); y = static_cast(static_cast(frame_height) * command->GetY()); if (command->IsProbeRect()) { width = static_cast(static_cast(frame_width) * command->GetWidth()); height = static_cast(static_cast(frame_height) * command->GetHeight()); } } else { x = static_cast(command->GetX()); y = static_cast(command->GetY()); width = static_cast(command->GetWidth()); height = static_cast(command->GetHeight()); } if (x + width > frame_width || y + height > frame_height) { return Result( "Line " + std::to_string(command->GetLine()) + ": Verifier::Probe Position(" + std::to_string(x + width - 1) + ", " + std::to_string(y + height - 1) + ") is out of framebuffer scope (" + std::to_string(frame_width) + "," + std::to_string(frame_height) + ")"); } if (row_stride < frame_width * texel_stride) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier::Probe Row stride of " + std::to_string(row_stride) + " is too small for " + std::to_string(frame_width) + " texels of " + std::to_string(texel_stride) + " bytes each"); } double tolerance[4] = {0, 0, 0, 0}; bool is_tolerance_percent[4] = {0, 0, 0, 0}; SetupToleranceForTexels(command, tolerance, is_tolerance_percent); const uint8_t* ptr = static_cast(buf); uint32_t count_of_invalid_pixels = 0; uint32_t first_invalid_i = 0; uint32_t first_invalid_j = 0; std::vector actual_texel_values_on_failure; for (uint32_t j = 0; j < height; ++j) { const uint8_t* p = ptr + row_stride * (j + y) + texel_stride * x; for (uint32_t i = 0; i < width; ++i) { auto actual_texel_values = GetActualValuesFromTexel(p + texel_stride * i, framebuffer_format); ScaleTexelValuesIfNeeded(&actual_texel_values, framebuffer_format); if (!IsTexelEqualToExpected(actual_texel_values, framebuffer_format, command, tolerance, is_tolerance_percent)) { if (!count_of_invalid_pixels) { actual_texel_values_on_failure = GetTexelInRGBA(actual_texel_values, framebuffer_format); first_invalid_i = i; first_invalid_j = j; } ++count_of_invalid_pixels; } } } if (count_of_invalid_pixels) { const auto& component = framebuffer_format->GetComponents().back(); float scale_factor_for_error_report = 1.0f; if (component.mode == FormatMode::kUNorm || component.mode == FormatMode::kSNorm || component.mode == FormatMode::kSRGB) { scale_factor_for_error_report = 255.0f; } return Result( "Line " + std::to_string(command->GetLine()) + ": Probe failed at: " + std::to_string(x + first_invalid_i) + ", " + std::to_string(first_invalid_j + y) + "\n" + " Expected RGBA: " + std::to_string(command->GetR() * scale_factor_for_error_report) + ", " + std::to_string(command->GetG() * scale_factor_for_error_report) + ", " + std::to_string(command->GetB() * scale_factor_for_error_report) + (command->IsRGBA() ? ", " + std::to_string(command->GetA() * scale_factor_for_error_report) + "\n Actual RGBA: " : "\n Actual RGB: ") + std::to_string(static_cast(actual_texel_values_on_failure[0]) * scale_factor_for_error_report) + ", " + std::to_string(static_cast(actual_texel_values_on_failure[1]) * scale_factor_for_error_report) + ", " + std::to_string(static_cast(actual_texel_values_on_failure[2]) * scale_factor_for_error_report) + (command->IsRGBA() ? ", " + std::to_string(static_cast( actual_texel_values_on_failure[3]) * scale_factor_for_error_report) : "") + "\n" + "Probe failed in " + std::to_string(count_of_invalid_pixels) + " pixels"); } return {}; } Result Verifier::ProbeSSBO(const ProbeSSBOCommand* command, uint32_t buffer_element_count, const void* buffer) { const auto& values = command->GetValues(); if (!buffer) { if (values.empty()) return {}; return Result( "Verifier::ProbeSSBO actual data is empty while expected " "data is not"); } auto* fmt = command->GetFormat(); size_t elem_count = values.size() / fmt->InputNeededPerElement(); size_t offset = static_cast(command->GetOffset()); size_t size_in_bytes = buffer_element_count * fmt->SizeInBytes(); if ((elem_count * fmt->SizeInBytes()) + offset > size_in_bytes) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier::ProbeSSBO request to access to byte " + std::to_string((elem_count * fmt->SizeInBytes()) + offset) + " would read outside buffer of size " + std::to_string(size_in_bytes) + " bytes"); } if (offset % fmt->SizeInBytes() != 0) { return Result("Line " + std::to_string(command->GetLine()) + ": Verifier::ProbeSSBO given offset (" + std::to_string(offset) + ") " + "is not multiple of element size (" + std::to_string(fmt->SizeInBytes()) + ")"); } const uint8_t* ptr = static_cast(buffer) + offset; if (fmt->IsInt8()) return CheckValue(command, ptr, values); if (fmt->IsUint8()) return CheckValue(command, ptr, values); if (fmt->IsInt16()) return CheckValue(command, ptr, values); if (fmt->IsUint16()) return CheckValue(command, ptr, values); if (fmt->IsInt32()) return CheckValue(command, ptr, values); if (fmt->IsUint32()) return CheckValue(command, ptr, values); if (fmt->IsInt64()) return CheckValue(command, ptr, values); if (fmt->IsUint64()) return CheckValue(command, ptr, values); if (fmt->IsFloat()) return CheckValue(command, ptr, values); if (fmt->IsDouble()) return CheckValue(command, ptr, values); return Result("Line " + std::to_string(command->GetLine()) + ": Verifier::ProbeSSBO unknown datum type"); } } // namespace amber