blob: a787e9d266ec38e67861e4447aaae72fb11824a0 [file] [log] [blame]
// 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 <cassert>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include "src/command.h"
namespace amber {
namespace {
const uint32_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,
uint32_t src_bit_offset,
uint32_t bits) {
while (src_bit_offset > 7) {
++src;
src_bit_offset = src_bit_offset - kBitsPerByte;
}
// Number of bytes greater than or equal to |(src_bit_offset + bits) / 8|.
const uint32_t size_in_bytes = (src_bit_offset + bits + 7) / kBitsPerByte;
assert(size_in_bytes <= kBitsPerByte);
uint64_t data = 0;
uint8_t* ptr = reinterpret_cast<uint8_t*>(&data);
for (uint32_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<size_t>((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<uint32_t>(value[1]) & 0x80) << 24U;
uint32_t exponent = (((static_cast<uint32_t>(value[1]) & 0x7c) >> 2U) + 112U)
<< 23U;
uint32_t mantissa = ((static_cast<uint32_t>(value[1]) & 0x3) << 8U |
static_cast<uint32_t>(value[0]))
<< 13U;
uint32_t hex = sign | exponent | mantissa;
float* hex_float = reinterpret_cast<float*>(&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<uint32_t>(value[1]) << 2U) |
((static_cast<uint32_t>(value[0]) & 0xc0) >> 6U)) +
112U)
<< 23U;
uint32_t mantissa = (static_cast<uint32_t>(value[0]) & 0x3f) << 17U;
uint32_t hex = exponent | mantissa;
float* hex_float = reinterpret_cast<float*>(&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<uint32_t>(value[1]) << 3U) |
((static_cast<uint32_t>(value[0]) & 0xe0) >> 5U)) +
112U)
<< 23U;
uint32_t mantissa = (static_cast<uint32_t>(value[0]) & 0x1f) << 18U;
uint32_t hex = exponent | mantissa;
float* hex_float = reinterpret_cast<float*>(&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 <typename T>
Result CheckValue(const ProbeSSBOCommand* command,
const uint8_t* memory,
const Value& value) {
const auto comp = command->GetComparator();
const auto& tolerance = command->GetTolerances();
const T* ptr = reinterpret_cast<const T*>(memory);
const T val = value.IsInteger() ? static_cast<T>(value.AsUint64())
: static_cast<T>(value.AsDouble());
switch (comp) {
case ProbeSSBOCommand::Comparator::kEqual:
if (value.IsInteger()) {
if (static_cast<uint64_t>(*ptr) != static_cast<uint64_t>(val)) {
return Result(std::to_string(*ptr) + " == " + std::to_string(val));
}
} else {
if (!IsEqualWithTolerance(static_cast<const double>(*ptr),
static_cast<const double>(val), kEpsilon)) {
return Result(std::to_string(*ptr) + " == " + std::to_string(val));
}
}
break;
case ProbeSSBOCommand::Comparator::kNotEqual:
if (value.IsInteger()) {
if (static_cast<uint64_t>(*ptr) == static_cast<uint64_t>(val)) {
return Result(std::to_string(*ptr) + " != " + std::to_string(val));
}
} else {
if (IsEqualWithTolerance(static_cast<const double>(*ptr),
static_cast<const double>(val), kEpsilon)) {
return Result(std::to_string(*ptr) + " != " + std::to_string(val));
}
}
break;
case ProbeSSBOCommand::Comparator::kFuzzyEqual:
if (!IsEqualWithTolerance(
static_cast<const double>(*ptr), static_cast<const double>(val),
command->HasTolerances() ? tolerance[0].value : kEpsilon,
command->HasTolerances() ? tolerance[0].is_percent : true)) {
return Result(std::to_string(*ptr) + " ~= " + std::to_string(val));
}
break;
case ProbeSSBOCommand::Comparator::kLess:
if (*ptr >= val)
return Result(std::to_string(*ptr) + " < " + std::to_string(val));
break;
case ProbeSSBOCommand::Comparator::kLessOrEqual:
if (*ptr > val)
return Result(std::to_string(*ptr) + " <= " + std::to_string(val));
break;
case ProbeSSBOCommand::Comparator::kGreater:
if (*ptr <= val)
return Result(std::to_string(*ptr) + " > " + std::to_string(val));
break;
case ProbeSSBOCommand::Comparator::kGreaterOrEqual:
if (*ptr < val)
return Result(std::to_string(*ptr) + " >= " + std::to_string(val));
break;
}
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 |fmt|.
std::vector<double> GetActualValuesFromTexel(const uint8_t* texel,
const Format* fmt) {
assert(fmt && !fmt->GetSegments().empty());
std::vector<double> actual_values(fmt->GetSegments().size());
uint32_t bit_offset = 0;
for (size_t i = 0; i < fmt->GetSegments().size(); ++i) {
const auto& seg = fmt->GetSegments()[i];
if (seg.IsPadding()) {
bit_offset += seg.GetNumBits();
continue;
}
uint8_t actual[8] = {0, 0, 0, 0, 0, 0, 0, 0};
uint32_t num_bits = seg.GetNumBits();
CopyBitsOfMemoryToBuffer(actual, texel, bit_offset, num_bits);
FormatMode mode = seg.GetFormatMode();
if (type::Type::IsInt8(mode, num_bits)) {
int8_t* ptr8 = nullptr;
ptr8 = reinterpret_cast<int8_t*>(actual);
actual_values[i] = static_cast<double>(*ptr8);
} else if (type::Type::IsInt16(mode, num_bits)) {
int16_t* ptr16 = nullptr;
ptr16 = reinterpret_cast<int16_t*>(actual);
actual_values[i] = static_cast<double>(*ptr16);
} else if (type::Type::IsInt32(mode, num_bits)) {
int32_t* ptr32 = nullptr;
ptr32 = reinterpret_cast<int32_t*>(actual);
actual_values[i] = static_cast<double>(*ptr32);
} else if (type::Type::IsInt64(mode, num_bits)) {
int64_t* ptr64 = nullptr;
ptr64 = reinterpret_cast<int64_t*>(actual);
actual_values[i] = static_cast<double>(*ptr64);
} else if (type::Type::IsUint8(mode, num_bits)) {
actual_values[i] = static_cast<double>(*actual);
} else if (type::Type::IsUint16(mode, num_bits)) {
uint16_t* ptr16 = nullptr;
ptr16 = reinterpret_cast<uint16_t*>(actual);
actual_values[i] = static_cast<double>(*ptr16);
} else if (type::Type::IsUint32(mode, num_bits)) {
uint32_t* ptr32 = nullptr;
ptr32 = reinterpret_cast<uint32_t*>(actual);
actual_values[i] = static_cast<double>(*ptr32);
} else if (type::Type::IsUint64(mode, num_bits)) {
uint64_t* ptr64 = nullptr;
ptr64 = reinterpret_cast<uint64_t*>(actual);
actual_values[i] = static_cast<double>(*ptr64);
} else if (type::Type::IsFloat32(mode, num_bits)) {
float* ptr = reinterpret_cast<float*>(actual);
actual_values[i] = static_cast<double>(*ptr);
} else if (type::Type::IsFloat64(mode, num_bits)) {
double* ptr = reinterpret_cast<double*>(actual);
actual_values[i] = *ptr;
} else if (type::Type::IsFloat(mode) && num_bits < 32) {
actual_values[i] = static_cast<double>(
HexFloatToFloat(actual, static_cast<uint8_t>(num_bits)));
} else {
assert(false && "Incorrect number of bits for number.");
}
bit_offset += num_bits;
}
return actual_values;
}
// If component mode of |fmt| 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<double>* texel, const Format* fmt) {
assert(fmt->GetSegments().size() == texel->size());
for (size_t i = 0; i < fmt->GetSegments().size(); ++i) {
const auto& seg = fmt->GetSegments()[i];
if (seg.IsPadding())
continue;
double scaled_value = (*texel)[i];
if (seg.GetFormatMode() == FormatMode::kUNorm) {
scaled_value /= static_cast<double>((1 << seg.GetNumBits()) - 1);
} else if (seg.GetFormatMode() == FormatMode::kSNorm) {
scaled_value /= static_cast<double>((1 << (seg.GetNumBits() - 1)) - 1);
} else if (seg.GetFormatMode() == FormatMode::kSRGB) {
scaled_value /= static_cast<double>((1 << seg.GetNumBits()) - 1);
if (seg.GetName() != FormatComponentType::kA)
scaled_value = SRGBToLinearValue(scaled_value);
} else if (seg.GetFormatMode() == FormatMode::kSScaled ||
seg.GetFormatMode() == FormatMode::kUScaled) {
assert(false && "UScaled and SScaled are not implemented");
}
(*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<double>& texel,
const Format* fmt,
const ProbeCommand* command,
const double* tolerance,
const bool* is_tolerance_percent) {
for (size_t i = 0; i < fmt->GetSegments().size(); ++i) {
const auto& seg = fmt->GetSegments()[i];
if (seg.IsPadding())
continue;
double texel_for_component = texel[i];
double expected = 0;
double current_tolerance = 0;
bool is_current_tolerance_percent = false;
switch (seg.GetName()) {
case FormatComponentType::kA:
if (!command->IsRGBA())
continue;
expected = static_cast<double>(command->GetA());
current_tolerance = tolerance[3];
is_current_tolerance_percent = is_tolerance_percent[3];
break;
case FormatComponentType::kR:
expected = static_cast<double>(command->GetR());
current_tolerance = tolerance[0];
is_current_tolerance_percent = is_tolerance_percent[0];
break;
case FormatComponentType::kG:
expected = static_cast<double>(command->GetG());
current_tolerance = tolerance[1];
is_current_tolerance_percent = is_tolerance_percent[1];
break;
case FormatComponentType::kB:
expected = static_cast<double>(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<double> GetTexelInRGBA(const std::vector<double>& texel,
const Format* fmt) {
std::vector<double> texel_in_rgba(texel.size());
for (size_t i = 0; i < fmt->GetSegments().size(); ++i) {
const auto& seg = fmt->GetSegments()[i];
if (seg.IsPadding())
continue;
switch (seg.GetName()) {
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* fmt,
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 (!fmt)
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<uint32_t>(static_cast<float>(frame_width) *
command->GetX());
y = static_cast<uint32_t>(static_cast<float>(frame_height) *
command->GetY());
if (command->IsProbeRect()) {
width = static_cast<uint32_t>(static_cast<float>(frame_width) *
command->GetWidth());
height = static_cast<uint32_t>(static_cast<float>(frame_height) *
command->GetHeight());
}
} else {
x = static_cast<uint32_t>(command->GetX());
y = static_cast<uint32_t>(command->GetY());
width = static_cast<uint32_t>(command->GetWidth());
height = static_cast<uint32_t>(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<const uint8_t*>(buf);
uint32_t count_of_invalid_pixels = 0;
uint32_t first_invalid_i = 0;
uint32_t first_invalid_j = 0;
std::vector<double> failure_values;
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, fmt);
ScaleTexelValuesIfNeeded(&actual_texel_values, fmt);
if (!IsTexelEqualToExpected(actual_texel_values, fmt, command, tolerance,
is_tolerance_percent)) {
if (!count_of_invalid_pixels) {
failure_values = GetTexelInRGBA(actual_texel_values, fmt);
first_invalid_i = i;
first_invalid_j = j;
}
++count_of_invalid_pixels;
}
}
}
if (count_of_invalid_pixels) {
float scale = fmt->IsNormalized() ? 255.f : 1.f;
std::string reason =
"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: " + std::to_string(command->GetR() * scale) + ", " +
std::to_string(command->GetG() * scale) + ", " +
std::to_string(command->GetB() * scale);
if (command->IsRGBA()) {
reason += ", " + std::to_string(command->GetA() * scale);
}
reason +=
"\n Actual: " +
std::to_string(static_cast<float>(failure_values[0]) * scale) + ", " +
std::to_string(static_cast<float>(failure_values[1]) * scale) + ", " +
std::to_string(static_cast<float>(failure_values[2]) * scale);
if (command->IsRGBA()) {
reason +=
", " + std::to_string(static_cast<float>(failure_values[3]) * scale);
}
reason += "\nProbe failed in " + std::to_string(count_of_invalid_pixels) +
" pixels";
return Result(reason);
}
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<size_t>(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()) + ")");
}
auto& segments = fmt->GetSegments();
const uint8_t* ptr = static_cast<const uint8_t*>(buffer) + offset;
for (size_t i = 0, k = 0; i < values.size(); ++i, ++k) {
if (k >= segments.size())
k = 0;
const auto& value = values[i];
auto segment = segments[k];
// Skip over any padding bytes.
while (segment.IsPadding()) {
ptr += segment.PaddingBytes();
++k;
if (k >= segments.size())
k = 0;
segment = segments[k];
}
Result r;
FormatMode mode = segment.GetFormatMode();
uint32_t num_bits = segment.GetNumBits();
if (type::Type::IsInt8(mode, num_bits))
r = CheckValue<int8_t>(command, ptr, value);
else if (type::Type::IsUint8(mode, num_bits))
r = CheckValue<uint8_t>(command, ptr, value);
else if (type::Type::IsInt16(mode, num_bits))
r = CheckValue<int16_t>(command, ptr, value);
else if (type::Type::IsUint16(mode, num_bits))
r = CheckValue<uint16_t>(command, ptr, value);
else if (type::Type::IsInt32(mode, num_bits))
r = CheckValue<int32_t>(command, ptr, value);
else if (type::Type::IsUint32(mode, num_bits))
r = CheckValue<uint32_t>(command, ptr, value);
else if (type::Type::IsInt64(mode, num_bits))
r = CheckValue<int64_t>(command, ptr, value);
else if (type::Type::IsUint64(mode, num_bits))
r = CheckValue<uint64_t>(command, ptr, value);
else if (type::Type::IsFloat32(mode, num_bits))
r = CheckValue<float>(command, ptr, value);
else if (type::Type::IsFloat64(mode, num_bits))
r = CheckValue<double>(command, ptr, value);
else
return Result("Unknown datum type");
if (!r.IsSuccess()) {
return Result("Line " + std::to_string(command->GetLine()) +
": Verifier failed: " + r.Error() + ", at index " +
std::to_string(i));
}
ptr += segment.SizeInBytes();
}
return {};
}
} // namespace amber