mirror of
https://github.com/RPCSX/rpcsx.git
synced 2026-01-21 16:10:47 +01:00
amdgpu: simplify evaluator
This commit is contained in:
parent
d361dfcaf0
commit
de7c40d330
|
|
@ -453,20 +453,16 @@ Cache::ShaderResources::eval(ir::InstructionId instId,
|
|||
case 8:
|
||||
result = readPointer<std::uint64_t>(address);
|
||||
break;
|
||||
case 12:
|
||||
result = readPointer<u32vec3>(address);
|
||||
break;
|
||||
case 16:
|
||||
result = readPointer<u32vec4>(address);
|
||||
break;
|
||||
case 32:
|
||||
result = readPointer<std::array<std::uint32_t, 8>>(address);
|
||||
break;
|
||||
case 64:
|
||||
result = readPointer<std::array<std::uint32_t, 16>>(address);
|
||||
break;
|
||||
|
||||
default:
|
||||
rx::die("unexpected pointer load size {}", loadSize);
|
||||
rx::dieIf(loadSize % sizeof(std::uint32_t), "unaligned load size {}",
|
||||
loadSize);
|
||||
|
||||
for (std::int32_t offset = 0; offset < loadSize;
|
||||
offset += sizeof(std::uint32_t)) {
|
||||
result.add(readPointer<std::uint32_t>(address + offset));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -2368,7 +2364,7 @@ Cache::Shader Cache::GraphicsTag::getShader(
|
|||
std::bit_cast<std::uint32_t>(context.paClVports[slot.data].zScale);
|
||||
break;
|
||||
case gcn::ConfigType::PsInputVGpr:
|
||||
if (slot.data > psVgprInput.size()) {
|
||||
if (slot.data >= psVgprInput.size()) {
|
||||
configPtr[index] = ~0;
|
||||
} else {
|
||||
configPtr[index] = std::bit_cast<std::uint32_t>(psVgprInput[slot.data]);
|
||||
|
|
|
|||
|
|
@ -2,33 +2,156 @@
|
|||
|
||||
#include "Vector.hpp"
|
||||
#include "ir/Value.hpp"
|
||||
#include "rx/align.hpp"
|
||||
#include "rx/die.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <variant>
|
||||
#include <cstring>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace shader::eval {
|
||||
struct Value {
|
||||
using Storage = std::variant<
|
||||
std::nullptr_t, std::int8_t, std::int16_t, std::int32_t, std::int64_t,
|
||||
std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t, float16_t,
|
||||
float32_t, float64_t, u8vec2, u8vec3, u8vec4, i8vec2, i8vec3, i8vec4,
|
||||
u16vec2, u16vec3, u16vec4, i16vec2, i16vec3, i16vec4, u32vec2, u32vec3,
|
||||
u32vec4, i32vec2, i32vec3, i32vec4, u64vec2, u64vec3, u64vec4, i64vec2,
|
||||
i64vec3, i64vec4, f32vec2, f32vec3, f32vec4, f64vec2, f64vec3, f64vec4,
|
||||
f16vec2, f16vec3, f16vec4, bool, bvec2, bvec3, bvec4,
|
||||
std::array<uint32_t, 8>, std::array<std::uint32_t, 16>>;
|
||||
static constexpr auto StorageSize = std::variant_size_v<Storage>;
|
||||
Storage storage;
|
||||
using Types = std::tuple<std::nullptr_t, bool, int8_t, int16_t, int32_t,
|
||||
int64_t, uint8_t, uint16_t, uint32_t, uint64_t,
|
||||
float16_t, float32_t, float64_t>;
|
||||
|
||||
static constexpr auto kMaxElementCount = 32;
|
||||
static constexpr auto kMaxTypeAlignment = [] {
|
||||
auto impl = []<std::size_t... I>(std::index_sequence<I...>) {
|
||||
std::size_t result = 1;
|
||||
((result = std::max(alignof(std::tuple_element_t<I, Types>), result)),
|
||||
...);
|
||||
return result;
|
||||
};
|
||||
return impl(std::make_index_sequence<std::tuple_size_v<Types>>{});
|
||||
}();
|
||||
static constexpr auto kMaxTypeSize = [] {
|
||||
auto impl = []<std::size_t... I>(std::index_sequence<I...>) {
|
||||
std::size_t result = 1;
|
||||
((result = std::max(sizeof(std::tuple_element_t<I, Types>), result)),
|
||||
...);
|
||||
return result;
|
||||
};
|
||||
return rx::alignUp(
|
||||
impl(std::make_index_sequence<std::tuple_size_v<Types>>{}),
|
||||
kMaxTypeAlignment);
|
||||
}();
|
||||
|
||||
template <typename T> static consteval std::size_t getTypeIndex() {
|
||||
auto impl = []<std::size_t... I>(std::index_sequence<I...>) {
|
||||
std::size_t result = -1;
|
||||
((result =
|
||||
std::is_same_v<T, std::tuple_element_t<I, Types>> ? I : result),
|
||||
...);
|
||||
return result;
|
||||
};
|
||||
return impl(std::make_index_sequence<std::tuple_size_v<Types>>{});
|
||||
}
|
||||
|
||||
static constexpr auto StorageSize = std::tuple_size_v<Types>;
|
||||
|
||||
template <typename T, typename RT = std::invoke_result_t<
|
||||
T, std::span<std::tuple_element_t<0, Types>>>>
|
||||
RT visit(T &&cb) const {
|
||||
static constexpr auto table = [] {
|
||||
std::array<RT (*)(void *cb, const char *data, std::uint32_t count),
|
||||
std::tuple_size_v<Types>>
|
||||
result;
|
||||
|
||||
auto impl = [&]<std::size_t... I>(std::index_sequence<I...>) {
|
||||
((result[I] = [](void *cb, const char *data,
|
||||
std::uint32_t count) -> RT {
|
||||
return (*reinterpret_cast<T *>(cb))(std::span(
|
||||
reinterpret_cast<const std::tuple_element_t<I, Types> *>(data),
|
||||
count));
|
||||
}),
|
||||
...);
|
||||
};
|
||||
|
||||
impl(std::make_index_sequence<std::tuple_size_v<Types>>{});
|
||||
|
||||
return result;
|
||||
}();
|
||||
|
||||
return table[mTypeIndex](&cb, mData, mCount);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t getConstituentSize() const {
|
||||
return visit([](auto values) -> std::size_t {
|
||||
if constexpr (std::is_same_v<decltype(values[0]), std::nullptr_t>) {
|
||||
return 0;
|
||||
} else {
|
||||
return sizeof(values[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
explicit operator bool() const { return !empty(); }
|
||||
bool empty() const { return storage.index() == 0; }
|
||||
[[nodiscard]] bool empty() const {
|
||||
return mCount == 0 || mTypeIndex == getTypeIndex<std::nullptr_t>();
|
||||
}
|
||||
[[nodiscard]] std::size_t size() const { return mCount; }
|
||||
|
||||
Value() : storage(nullptr) {}
|
||||
Value() = default;
|
||||
|
||||
template <typename FT, typename... T>
|
||||
requires(getTypeIndex<FT>() < std::tuple_size_v<Types> &&
|
||||
(std::is_same_v<FT, T> && ...))
|
||||
Value(FT firstValue, T... value) {
|
||||
add(firstValue);
|
||||
(add(value), ...);
|
||||
}
|
||||
|
||||
void add(const Value &value) {
|
||||
if (value.mCount != 1) {
|
||||
mTypeIndex = getTypeIndex<std::nullptr_t>();
|
||||
mCount = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCount == 0) {
|
||||
mTypeIndex = value.mTypeIndex;
|
||||
mCount = 1;
|
||||
std::memcpy(mData, value.mData, kMaxTypeSize);
|
||||
return;
|
||||
}
|
||||
|
||||
rx::dieIf(mCount >= kMaxElementCount, "storage too small");
|
||||
|
||||
if (mTypeIndex != value.mTypeIndex) {
|
||||
mTypeIndex = getTypeIndex<std::nullptr_t>();
|
||||
mCount = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
auto index = mCount++;
|
||||
auto elemSize = getConstituentSize();
|
||||
std::memcpy(mData + elemSize * index, value.mData, kMaxTypeSize);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
Value(T &&value)
|
||||
requires requires { Storage(std::forward<T>(value)); }
|
||||
: storage(std::forward<T>(value)) {}
|
||||
requires(getTypeIndex<T>() < std::tuple_size_v<Types>)
|
||||
void add(T value) {
|
||||
if (mCount == 0) {
|
||||
mTypeIndex = getTypeIndex<T>();
|
||||
mCount = 1;
|
||||
*getData<T>() = value;
|
||||
return;
|
||||
}
|
||||
|
||||
rx::dieIf(mCount >= kMaxElementCount, "storage too small");
|
||||
|
||||
if (mTypeIndex != getTypeIndex<T>()) {
|
||||
mTypeIndex = getTypeIndex<std::nullptr_t>();
|
||||
mCount = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
getData<T>()[mCount++] = value;
|
||||
}
|
||||
|
||||
static Value compositeConstruct(ir::Value type,
|
||||
std::span<const Value> constituents);
|
||||
|
|
@ -52,21 +175,43 @@ struct Value {
|
|||
std::optional<std::int64_t> sExtScalar() const;
|
||||
|
||||
template <typename T>
|
||||
requires requires { std::get<T>(storage); }
|
||||
T get() const {
|
||||
return std::get<T>(storage);
|
||||
requires(getTypeIndex<T>() < std::tuple_size_v<Types>)
|
||||
[[nodiscard]] const T &get(std::size_t index = 0) const {
|
||||
rx::dieIf(mTypeIndex != getTypeIndex<T>(),
|
||||
"eval::Value::get(): invalid type");
|
||||
rx::dieIf(index >= std::tuple_size_v<Types>,
|
||||
"eval::Value::get(): invalid index");
|
||||
return getData<T>()[index];
|
||||
}
|
||||
|
||||
template <auto I>
|
||||
requires(I < std::tuple_size_v<Types>)
|
||||
[[nodiscard]] const std::tuple_element_t<I, Types> &
|
||||
get(std::size_t index = 0) const {
|
||||
rx::dieIf(mTypeIndex != I, "eval::Value::get(): invalid type");
|
||||
rx::dieIf(index >= std::tuple_size_v<Types>,
|
||||
"eval::Value::get(): invalid index");
|
||||
return getData<std::tuple_element_t<I, Types>>()[index];
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires requires { std::get<T>(storage); }
|
||||
std::optional<T> as() const {
|
||||
if (auto result = std::get_if<T>(&storage)) {
|
||||
return *result;
|
||||
requires(getTypeIndex<T>() < std::tuple_size_v<Types>)
|
||||
[[nodiscard]] std::optional<T> as(std::size_t index = 0) const {
|
||||
rx::dieIf(index >= std::tuple_size_v<Types>,
|
||||
"eval::Value::as(): invalid index");
|
||||
if (mTypeIndex == getTypeIndex<T>()) {
|
||||
return getData<T>()[index];
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires(getTypeIndex<T>() < std::tuple_size_v<Types>)
|
||||
[[nodiscard]] bool is() const {
|
||||
return mTypeIndex == getTypeIndex<T>();
|
||||
}
|
||||
|
||||
Value operator+(const Value &rhs) const;
|
||||
Value operator-(const Value &rhs) const;
|
||||
Value operator*(const Value &rhs) const;
|
||||
|
|
@ -89,5 +234,17 @@ struct Value {
|
|||
Value operator-() const;
|
||||
Value operator~() const;
|
||||
Value operator!() const;
|
||||
|
||||
std::size_t index() const { return mTypeIndex; }
|
||||
|
||||
private:
|
||||
template <typename T> T *getData() { return reinterpret_cast<T *>(mData); }
|
||||
template <typename T> const T *getData() const {
|
||||
return reinterpret_cast<const T *>(mData);
|
||||
}
|
||||
|
||||
std::uint32_t mTypeIndex = 0;
|
||||
std::uint32_t mCount = 0;
|
||||
alignas(kMaxTypeAlignment) char mData[kMaxTypeSize * kMaxElementCount];
|
||||
};
|
||||
} // namespace shader::eval
|
||||
|
|
|
|||
|
|
@ -2,132 +2,15 @@
|
|||
#include "dialect.hpp"
|
||||
#include "ir.hpp"
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <type_traits>
|
||||
|
||||
using namespace shader;
|
||||
|
||||
template <typename Cond, typename... Args> consteval bool testVisitCond() {
|
||||
if constexpr (std::is_same_v<Cond, void>) {
|
||||
return true;
|
||||
} else {
|
||||
return Cond{}(std::remove_cvref_t<Args>{}...);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Cond, std::size_t U> consteval bool testVisitCond() {
|
||||
if constexpr (U >= eval::Value::StorageSize) {
|
||||
return false;
|
||||
} else if constexpr (std::is_same_v<Cond, void>) {
|
||||
return true;
|
||||
} else {
|
||||
return Cond{}(std::variant_alternative_t<U, eval::Value::Storage>{});
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Cond = void, size_t I = 0>
|
||||
constexpr eval::Value visitImpl(const eval::Value &variant, auto &&fn) {
|
||||
|
||||
#define DEFINE_CASE(N) \
|
||||
case I + N: \
|
||||
if constexpr (testVisitCond<Cond, I + N>()) { \
|
||||
return std::forward<decltype(fn)>(fn)(std::get<I + N>(variant.storage)); \
|
||||
} else { \
|
||||
return {}; \
|
||||
}
|
||||
|
||||
switch (variant.storage.index()) {
|
||||
DEFINE_CASE(0);
|
||||
DEFINE_CASE(1);
|
||||
DEFINE_CASE(2);
|
||||
DEFINE_CASE(3);
|
||||
DEFINE_CASE(4);
|
||||
DEFINE_CASE(5);
|
||||
DEFINE_CASE(6);
|
||||
DEFINE_CASE(7);
|
||||
DEFINE_CASE(8);
|
||||
DEFINE_CASE(9);
|
||||
DEFINE_CASE(10);
|
||||
DEFINE_CASE(11);
|
||||
DEFINE_CASE(12);
|
||||
DEFINE_CASE(13);
|
||||
DEFINE_CASE(14);
|
||||
DEFINE_CASE(15);
|
||||
DEFINE_CASE(16);
|
||||
DEFINE_CASE(17);
|
||||
DEFINE_CASE(18);
|
||||
DEFINE_CASE(19);
|
||||
DEFINE_CASE(20);
|
||||
DEFINE_CASE(21);
|
||||
DEFINE_CASE(22);
|
||||
DEFINE_CASE(23);
|
||||
DEFINE_CASE(24);
|
||||
DEFINE_CASE(25);
|
||||
DEFINE_CASE(26);
|
||||
DEFINE_CASE(27);
|
||||
DEFINE_CASE(28);
|
||||
DEFINE_CASE(29);
|
||||
DEFINE_CASE(30);
|
||||
DEFINE_CASE(31);
|
||||
DEFINE_CASE(32);
|
||||
DEFINE_CASE(33);
|
||||
DEFINE_CASE(34);
|
||||
DEFINE_CASE(35);
|
||||
DEFINE_CASE(36);
|
||||
DEFINE_CASE(37);
|
||||
DEFINE_CASE(38);
|
||||
DEFINE_CASE(39);
|
||||
DEFINE_CASE(40);
|
||||
DEFINE_CASE(41);
|
||||
DEFINE_CASE(42);
|
||||
DEFINE_CASE(43);
|
||||
DEFINE_CASE(44);
|
||||
DEFINE_CASE(45);
|
||||
DEFINE_CASE(46);
|
||||
DEFINE_CASE(47);
|
||||
DEFINE_CASE(48);
|
||||
DEFINE_CASE(49);
|
||||
DEFINE_CASE(50);
|
||||
DEFINE_CASE(51);
|
||||
DEFINE_CASE(52);
|
||||
DEFINE_CASE(53);
|
||||
DEFINE_CASE(54);
|
||||
DEFINE_CASE(55);
|
||||
DEFINE_CASE(56);
|
||||
DEFINE_CASE(57);
|
||||
DEFINE_CASE(58);
|
||||
DEFINE_CASE(59);
|
||||
DEFINE_CASE(60);
|
||||
DEFINE_CASE(61);
|
||||
DEFINE_CASE(62);
|
||||
DEFINE_CASE(63);
|
||||
}
|
||||
#undef DEFINE_CASE
|
||||
|
||||
constexpr auto NextIndex = I + 64;
|
||||
|
||||
if constexpr (NextIndex < eval::Value::StorageSize) {
|
||||
return visitImpl<Cond, NextIndex>(std::forward<decltype(fn)>(fn),
|
||||
std::forward<decltype(variant)>(variant));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
template <typename Cond = void, typename Cb>
|
||||
constexpr eval::Value visitScalarType(ir::Value type, Cb &&cb)
|
||||
requires requires {
|
||||
{ std::forward<Cb>(cb)(int{}) } -> std::same_as<eval::Value>;
|
||||
}
|
||||
{
|
||||
auto invoke = [&](auto type) -> eval::Value {
|
||||
if constexpr (testVisitCond<Cond, std::remove_cvref_t<decltype(type)>>()) {
|
||||
return std::forward<Cb>(cb)(type);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
constexpr bool invokeWithType(ir::Value type, T &&invoke) {
|
||||
if (type == ir::spv::OpTypeBool) {
|
||||
return invoke(bool{});
|
||||
invoke(bool{});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == ir::spv::OpTypeInt) {
|
||||
|
|
@ -136,213 +19,106 @@ constexpr eval::Value visitScalarType(ir::Value type, Cb &&cb)
|
|||
switch (*type.getOperand(0).getAsInt32()) {
|
||||
case 8:
|
||||
if (isSigned) {
|
||||
return invoke(std::int8_t{});
|
||||
invoke(std::int8_t{});
|
||||
return true;
|
||||
}
|
||||
return invoke(std::uint8_t{});
|
||||
invoke(std::uint8_t{});
|
||||
return true;
|
||||
|
||||
case 16:
|
||||
if (isSigned) {
|
||||
return invoke(std::int16_t{});
|
||||
invoke(std::int16_t{});
|
||||
return true;
|
||||
}
|
||||
return invoke(std::uint16_t{});
|
||||
invoke(std::uint16_t{});
|
||||
return true;
|
||||
|
||||
case 32:
|
||||
if (isSigned) {
|
||||
return invoke(std::int32_t{});
|
||||
invoke(std::int32_t{});
|
||||
return true;
|
||||
}
|
||||
return invoke(std::uint32_t{});
|
||||
invoke(std::uint32_t{});
|
||||
return true;
|
||||
|
||||
case 64:
|
||||
if (isSigned) {
|
||||
return invoke(std::int64_t{});
|
||||
invoke(std::int64_t{});
|
||||
return true;
|
||||
}
|
||||
return invoke(std::uint64_t{});
|
||||
invoke(std::uint64_t{});
|
||||
return true;
|
||||
}
|
||||
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type == ir::spv::OpTypeFloat) {
|
||||
switch (*type.getOperand(0).getAsInt32()) {
|
||||
case 16:
|
||||
return invoke(shader::float16_t{});
|
||||
invoke(shader::float16_t{});
|
||||
return true;
|
||||
|
||||
case 32:
|
||||
return invoke(shader::float32_t{});
|
||||
invoke(shader::float32_t{});
|
||||
return true;
|
||||
|
||||
case 64:
|
||||
return invoke(shader::float64_t{});
|
||||
invoke(shader::float64_t{});
|
||||
return true;
|
||||
}
|
||||
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
template <typename Cond = void, typename Cb>
|
||||
constexpr eval::Value visitType(ir::Value type, Cb &&cb)
|
||||
requires requires {
|
||||
{ std::forward<Cb>(cb)(int{}) } -> std::same_as<eval::Value>;
|
||||
}
|
||||
{
|
||||
if (type == ir::spv::OpTypeInt || type == ir::spv::OpTypeFloat ||
|
||||
type == ir::spv::OpTypeBool) {
|
||||
return visitScalarType<Cond>(type, cb);
|
||||
}
|
||||
|
||||
auto invoke = [&](auto type) -> eval::Value {
|
||||
if constexpr (testVisitCond<Cond, std::remove_cvref_t<decltype(type)>>()) {
|
||||
return std::forward<Cb>(cb)(type);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
if (type == ir::spv::OpTypeVector) {
|
||||
switch (*type.getOperand(1).getAsInt32()) {
|
||||
case 2:
|
||||
return visitScalarType(
|
||||
type.getOperand(0).getAsValue(),
|
||||
[&]<typename T>(T) { return invoke(shader::Vector<T, 2>{}); });
|
||||
|
||||
case 3:
|
||||
return visitScalarType(
|
||||
type.getOperand(0).getAsValue(),
|
||||
[&]<typename T>(T) { return invoke(shader::Vector<T, 3>{}); });
|
||||
|
||||
case 4:
|
||||
return visitScalarType(
|
||||
type.getOperand(0).getAsValue(),
|
||||
[&]<typename T>(T) { return invoke(shader::Vector<T, 4>{}); });
|
||||
}
|
||||
|
||||
return {};
|
||||
return invokeWithType(type.getOperand(0).getAsValue(),
|
||||
std::forward<T>(invoke));
|
||||
}
|
||||
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename Cond = void, typename Cb>
|
||||
eval::Value visit(const eval::Value &value, Cb &&cb) {
|
||||
using VisitCond = decltype([](auto &&storage) {
|
||||
using T = std::remove_cvref_t<decltype(storage)>;
|
||||
if constexpr (std::is_same_v<T, std::nullptr_t>) {
|
||||
return false;
|
||||
} else {
|
||||
return testVisitCond<Cond, T>();
|
||||
}
|
||||
static constexpr std::size_t getIrTypeIndex(ir::Value type) {
|
||||
std::size_t result = 0;
|
||||
|
||||
invokeWithType(type, [&](auto type) {
|
||||
result = eval::Value::getTypeIndex<decltype(type)>();
|
||||
});
|
||||
|
||||
return visitImpl<VisitCond>(value, std::forward<Cb>(cb));
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Cb>
|
||||
eval::Value visit2(auto &&cond, const eval::Value &value, Cb &&cb) {
|
||||
if constexpr (cond()) {
|
||||
return visitImpl(value, std::forward<Cb>(cb));
|
||||
} else {
|
||||
return {};
|
||||
static constexpr std::size_t getIrTypeConstituents(ir::Value type) {
|
||||
if (type == ir::spv::OpTypeVector) {
|
||||
return *type.getOperand(1).getAsInt32();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
template <typename ValueCond = void, typename TypeVisitCond = void,
|
||||
typename TypeValueVisitCond = void, typename Cb>
|
||||
eval::Value visitWithType(const eval::Value &value, ir::Value type, Cb &&cb) {
|
||||
using ValueVisitCond = decltype([](auto storage) {
|
||||
if constexpr (std::is_same_v<decltype(storage), std::nullptr_t>) {
|
||||
return false;
|
||||
} else {
|
||||
return testVisitCond<ValueCond, decltype(storage)>();
|
||||
}
|
||||
});
|
||||
|
||||
return visitImpl<ValueVisitCond>(value, [&](auto &&value) -> eval::Value {
|
||||
return visitType<TypeVisitCond>(type, [&](auto type) -> eval::Value {
|
||||
if constexpr (testVisitCond<TypeValueVisitCond, decltype(type),
|
||||
decltype(value)>()) {
|
||||
return std::forward<Cb>(cb)(type, value);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
namespace {
|
||||
template <typename T> struct ComponentTypeImpl {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T, std::size_t N> struct ComponentTypeImpl<Vector<T, N>> {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T, std::size_t N>
|
||||
struct ComponentTypeImpl<std::array<T, N>> {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T> struct MakeSignedImpl {
|
||||
using type = std::make_signed_t<T>;
|
||||
};
|
||||
|
||||
template <typename T, std::size_t N> struct MakeSignedImpl<Vector<T, N>> {
|
||||
using type = Vector<std::make_signed_t<T>, N>;
|
||||
};
|
||||
template <typename T> struct MakeUnsignedImpl {
|
||||
using type = std::make_unsigned_t<T>;
|
||||
};
|
||||
|
||||
template <typename T, std::size_t N> struct MakeUnsignedImpl<Vector<T, N>> {
|
||||
using type = Vector<std::make_unsigned_t<T>, N>;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
template <typename T> using ComponentType = typename ComponentTypeImpl<T>::type;
|
||||
template <typename T> using MakeSigned = typename MakeSignedImpl<T>::type;
|
||||
template <typename T> using MakeUnsigned = typename MakeUnsignedImpl<T>::type;
|
||||
|
||||
template <typename> constexpr std::size_t Components = 1;
|
||||
template <typename T, std::size_t N>
|
||||
constexpr std::size_t Components<Vector<T, N>> = N;
|
||||
template <typename T, std::size_t N>
|
||||
constexpr std::size_t Components<std::array<T, N>> = N;
|
||||
|
||||
template <typename> constexpr bool IsArray = false;
|
||||
template <typename T, std::size_t N>
|
||||
constexpr bool IsArray<std::array<T, N>> = true;
|
||||
|
||||
eval::Value
|
||||
eval::Value::compositeConstruct(ir::Value type,
|
||||
std::span<const eval::Value> constituents) {
|
||||
using Cond =
|
||||
decltype([](auto type) { return Components<decltype(type)> > 1; });
|
||||
if (getIrTypeConstituents(type) != constituents.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return visitType<Cond>(type, [&](auto type) -> Value {
|
||||
constexpr std::size_t N = Components<decltype(type)>;
|
||||
if (N != constituents.size()) {
|
||||
return {};
|
||||
}
|
||||
auto typeIndex = getIrTypeIndex(type);
|
||||
|
||||
decltype(type) result;
|
||||
eval::Value result;
|
||||
for (auto &elem : constituents) {
|
||||
result.add(elem);
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
if (auto value = constituents[i].as<ComponentType<decltype(type)>>()) {
|
||||
result[i] = *value;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (result.index() != typeIndex) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
eval::Value eval::Value::compositeExtract(const Value &index) const {
|
||||
using Cond =
|
||||
decltype([](auto type) { return Components<decltype(type)> > 1; });
|
||||
|
||||
auto optIndexInt = index.zExtScalar();
|
||||
if (!optIndexInt) {
|
||||
return {};
|
||||
|
|
@ -350,321 +126,249 @@ eval::Value eval::Value::compositeExtract(const Value &index) const {
|
|||
|
||||
auto indexInt = *optIndexInt;
|
||||
|
||||
return visit<Cond>(*this, [&](auto &&value) -> Value {
|
||||
using ValueType = std::remove_cvref_t<decltype(value)>;
|
||||
constexpr std::size_t N = Components<ValueType>;
|
||||
if (indexInt >= size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (indexInt >= N) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return value[indexInt];
|
||||
});
|
||||
eval::Value result;
|
||||
result.mTypeIndex = mTypeIndex;
|
||||
result.mCount = 1;
|
||||
std::memcpy(result.mData, mData + indexInt * getConstituentSize(),
|
||||
kMaxTypeSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
eval::Value eval::Value::isNan() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_floating_point_v<ComponentType<decltype(type)>> &&
|
||||
!IsArray<decltype(type)>;
|
||||
});
|
||||
return visit([](auto value) {
|
||||
eval::Value result;
|
||||
|
||||
return visit<Cond>(*this, [](auto &&value) -> Value {
|
||||
constexpr std::size_t N = Components<std::remove_cvref_t<decltype(value)>>;
|
||||
|
||||
if constexpr (N == 1) {
|
||||
return std::isnan(value);
|
||||
} else {
|
||||
Vector<bool, N> result;
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
result[i] = std::isnan(value[i]);
|
||||
if constexpr (std::is_floating_point_v<
|
||||
typename decltype(value)::value_type>) {
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
result.add(std::isnan(value[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
eval::Value eval::Value::isInf() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_floating_point_v<ComponentType<decltype(type)>> &&
|
||||
!IsArray<decltype(type)>;
|
||||
});
|
||||
return visit([](auto value) {
|
||||
eval::Value result;
|
||||
|
||||
return visit<Cond>(*this, [](auto &&value) -> Value {
|
||||
constexpr std::size_t N = Components<std::remove_cvref_t<decltype(value)>>;
|
||||
|
||||
if constexpr (N == 1) {
|
||||
return std::isinf(value);
|
||||
} else {
|
||||
Vector<bool, N> result;
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
result[i] = std::isinf(value[i]);
|
||||
if constexpr (std::is_floating_point_v<
|
||||
typename decltype(value)::value_type>) {
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
result.add(std::isinf(value[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
eval::Value eval::Value::isFinite() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_floating_point_v<ComponentType<decltype(type)>>;
|
||||
});
|
||||
return visit([](auto value) {
|
||||
eval::Value result;
|
||||
|
||||
return visit<Cond>(*this, [](auto &&value) -> Value {
|
||||
constexpr std::size_t N = Components<std::remove_cvref_t<decltype(value)>>;
|
||||
|
||||
if constexpr (N == 1) {
|
||||
return std::isfinite(value);
|
||||
} else {
|
||||
Vector<bool, N> result;
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
result[i] = std::isfinite(value[i]);
|
||||
if constexpr (std::is_floating_point_v<
|
||||
typename decltype(value)::value_type>) {
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
result.add(std::isfinite(value[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
eval::Value eval::Value::makeUnsigned() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_integral_v<ComponentType<decltype(type)>> &&
|
||||
!std::is_same_v<ComponentType<decltype(type)>, bool> &&
|
||||
!IsArray<decltype(type)>;
|
||||
});
|
||||
return visit([](auto value) {
|
||||
eval::Value result;
|
||||
using value_type = typename decltype(value)::value_type;
|
||||
|
||||
return visit<Cond>(*this, [](auto &&value) -> Value {
|
||||
constexpr std::size_t N = Components<std::remove_cvref_t<decltype(value)>>;
|
||||
using T = std::make_unsigned_t<
|
||||
ComponentType<std::remove_cvref_t<decltype(value)>>>;
|
||||
|
||||
if constexpr (N == 1) {
|
||||
return static_cast<T>(value);
|
||||
} else {
|
||||
Vector<T, N> result;
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
result[i] = static_cast<T>(value[i]);
|
||||
if constexpr (std::is_integral_v<value_type> &&
|
||||
!std::is_same_v<value_type, bool>) {
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
result.add(static_cast<std::make_unsigned_t<value_type>>(value[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
eval::Value eval::Value::makeSigned() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_integral_v<ComponentType<decltype(type)>> &&
|
||||
!std::is_same_v<ComponentType<decltype(type)>, bool> &&
|
||||
!IsArray<decltype(type)>;
|
||||
});
|
||||
return visit([](auto value) {
|
||||
eval::Value result;
|
||||
using value_type = typename decltype(value)::value_type;
|
||||
|
||||
return visit<Cond>(*this, [](auto &&value) -> Value {
|
||||
constexpr std::size_t N = Components<std::remove_cvref_t<decltype(value)>>;
|
||||
using T =
|
||||
std::make_signed_t<ComponentType<std::remove_cvref_t<decltype(value)>>>;
|
||||
|
||||
if constexpr (N == 1) {
|
||||
return static_cast<T>(value);
|
||||
} else {
|
||||
Vector<T, N> result;
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
result[i] = static_cast<T>(value[i]);
|
||||
if constexpr (std::is_integral_v<value_type> &&
|
||||
!std::is_same_v<value_type, bool>) {
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
result.add(static_cast<std::make_signed_t<value_type>>(value[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
eval::Value eval::Value::all() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_same_v<ComponentType<decltype(type)>, bool> &&
|
||||
(Components<decltype(type)> > 1) && !IsArray<decltype(type)>;
|
||||
});
|
||||
|
||||
return visit<Cond>(*this, [](auto &&value) {
|
||||
constexpr std::size_t N = Components<std::remove_cvref_t<decltype(value)>>;
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
if (!value[i]) {
|
||||
return false;
|
||||
return visit([](auto value) {
|
||||
if constexpr (std::is_same_v<typename decltype(value)::value_type, bool>) {
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
if (!value[i]) {
|
||||
return eval::Value(false);
|
||||
}
|
||||
}
|
||||
return eval::Value(true);
|
||||
}
|
||||
return true;
|
||||
|
||||
return eval::Value();
|
||||
});
|
||||
}
|
||||
|
||||
eval::Value eval::Value::any() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_same_v<ComponentType<decltype(type)>, bool> &&
|
||||
(Components<decltype(type)> > 1) && !IsArray<decltype(type)>;
|
||||
});
|
||||
|
||||
return visit<Cond>(*this, [](auto &&value) {
|
||||
constexpr std::size_t N = Components<std::remove_cvref_t<decltype(value)>>;
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
if (value[i]) {
|
||||
return true;
|
||||
return visit([](auto value) {
|
||||
if constexpr (std::is_same_v<typename decltype(value)::value_type, bool>) {
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
if (value[i]) {
|
||||
return eval::Value(true);
|
||||
}
|
||||
}
|
||||
return eval::Value(false);
|
||||
}
|
||||
return false;
|
||||
|
||||
return eval::Value();
|
||||
});
|
||||
}
|
||||
|
||||
eval::Value eval::Value::select(const Value &trueValue,
|
||||
const Value &falseValue) const {
|
||||
using Cond = decltype([](auto type) consteval {
|
||||
return std::is_same_v<ComponentType<decltype(type)>, bool> &&
|
||||
!IsArray<decltype(type)>;
|
||||
});
|
||||
auto optCond = as<bool>();
|
||||
if (!optCond) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return visit<Cond>(*this, [&](auto &&cond) -> Value {
|
||||
using CondType = std::remove_cvref_t<decltype(cond)>;
|
||||
using TrueCond = decltype([](auto type) consteval {
|
||||
return Components<decltype(type)> == Components<CondType>;
|
||||
});
|
||||
|
||||
return visit<TrueCond>(trueValue, [&](auto &&trueValue) {
|
||||
using TrueValue = std::remove_cvref_t<decltype(trueValue)>;
|
||||
using FalseCond = decltype([](auto type) {
|
||||
return std::is_same_v<TrueValue, std::remove_cvref_t<decltype(type)>>;
|
||||
});
|
||||
|
||||
return visit(falseValue, [&](auto &&falseValue) -> Value {
|
||||
if constexpr (std::is_same_v<TrueValue, std::remove_cvref_t<
|
||||
decltype(falseValue)>>) {
|
||||
constexpr std::size_t N = Components<CondType>;
|
||||
|
||||
if constexpr (N == 1) {
|
||||
return cond ? trueValue : falseValue;
|
||||
} else {
|
||||
Vector<bool, N> result;
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
result[i] = cond[i] ? trueValue[i] : falseValue[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
auto cond = *optCond;
|
||||
return cond ? trueValue : falseValue;
|
||||
}
|
||||
|
||||
eval::Value eval::Value::iConvert(ir::Value type, bool isSigned) const {
|
||||
using Cond = decltype([](auto type) {
|
||||
using Type = std::remove_cvref_t<decltype(type)>;
|
||||
eval::Value result;
|
||||
|
||||
return std::is_integral_v<ComponentType<Type>> &&
|
||||
!std::is_same_v<bool, ComponentType<Type>> &&
|
||||
!IsArray<decltype(type)>;
|
||||
});
|
||||
|
||||
using PairCond = decltype([](auto lhs, auto rhs) {
|
||||
using Lhs = decltype(lhs);
|
||||
using Rhs = decltype(rhs);
|
||||
|
||||
return !std::is_same_v<Lhs, Rhs> && Components<Lhs> == Components<Rhs>;
|
||||
});
|
||||
|
||||
return visitWithType<Cond, Cond, PairCond>(
|
||||
*this, type, [&](auto type, auto &&value) -> Value {
|
||||
using Type = std::remove_cvref_t<decltype(type)>;
|
||||
using ValueType = std::remove_cvref_t<decltype(value)>;
|
||||
if (isSigned) {
|
||||
return static_cast<Type>(static_cast<MakeSigned<ValueType>>(value));
|
||||
} else {
|
||||
return static_cast<Type>(static_cast<MakeUnsigned<ValueType>>(value));
|
||||
(isSigned ? makeSigned() : makeUnsigned()).visit([&](auto value) {
|
||||
invokeWithType(type, [&](auto type) {
|
||||
if constexpr (std::is_integral_v<decltype(type)> &&
|
||||
!std::is_same_v<bool, decltype(type)> &&
|
||||
std::is_integral_v<typename decltype(value)::value_type> &&
|
||||
!std::is_same_v<bool,
|
||||
typename decltype(value)::value_type>) {
|
||||
for (auto item : value) {
|
||||
result.add(static_cast<decltype(type)>(item));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
eval::Value eval::Value::fConvert(ir::Value type) const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_floating_point_v<ComponentType<decltype(type)>> &&
|
||||
!IsArray<decltype(type)>;
|
||||
eval::Value result;
|
||||
|
||||
visit([&](auto value) {
|
||||
invokeWithType(type, [&](auto type) {
|
||||
if constexpr (std::is_floating_point_v<decltype(type)> &&
|
||||
std::is_floating_point_v<
|
||||
typename decltype(value)::value_type>) {
|
||||
for (auto item : value) {
|
||||
result.add(static_cast<decltype(type)>(item));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
using PairCond = decltype([](auto lhs, auto rhs) {
|
||||
using Lhs = decltype(lhs);
|
||||
using Rhs = decltype(rhs);
|
||||
|
||||
return !std::is_same_v<Lhs, Rhs> && Components<Lhs> == Components<Rhs>;
|
||||
});
|
||||
|
||||
return visitWithType<void, void, PairCond>(
|
||||
*this, type, [&](auto type, auto &&value) -> Value {
|
||||
using Type = std::remove_cvref_t<decltype(type)>;
|
||||
return static_cast<Type>(value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
eval::Value eval::Value::bitcast(ir::Value type) const {
|
||||
using Cond = decltype([](auto type, auto value) {
|
||||
using Type = std::remove_cvref_t<decltype(type)>;
|
||||
eval::Value result;
|
||||
|
||||
return sizeof(type) == sizeof(value);
|
||||
auto resultTypeElemCount = getIrTypeConstituents(type);
|
||||
visit([&](auto value) {
|
||||
invokeWithType(type, [&](auto resultType) {
|
||||
if (value.size_bytes() == sizeof(resultType) * resultTypeElemCount) {
|
||||
result.mTypeIndex = getIrTypeIndex(type);
|
||||
result.mCount = resultTypeElemCount;
|
||||
std::memcpy(result.mData, value.data(), value.size_bytes());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return visitWithType<void, void, Cond>(
|
||||
*this, type, [](auto type, auto &&value) -> Value {
|
||||
return std::bit_cast<decltype(type)>(value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<std::uint64_t> eval::Value::zExtScalar() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_integral_v<ComponentType<decltype(type)>> &&
|
||||
!std::is_same_v<ComponentType<decltype(type)>, bool> &&
|
||||
Components<decltype(type)> == 1 && !IsArray<decltype(type)>;
|
||||
});
|
||||
|
||||
auto result = visit<Cond>(*this, [&](auto value) -> Value {
|
||||
return static_cast<std::uint64_t>(
|
||||
static_cast<MakeUnsigned<decltype(value)>>(value));
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return result.as<std::uint64_t>();
|
||||
if (empty() || size() != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
return makeUnsigned().visit([](auto value) -> std::optional<std::uint64_t> {
|
||||
if constexpr (std::is_integral_v<typename decltype(value)::value_type>) {
|
||||
return static_cast<std::uint64_t>(value[0]);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<std::int64_t> eval::Value::sExtScalar() const {
|
||||
using Cond = decltype([](auto type) {
|
||||
return std::is_integral_v<ComponentType<decltype(type)>> &&
|
||||
!std::is_same_v<ComponentType<decltype(type)>, bool> &&
|
||||
Components<decltype(type)> == 1 && !IsArray<decltype(type)>;
|
||||
});
|
||||
|
||||
auto result = visit<Cond>(*this, [&](auto value) -> Value {
|
||||
return static_cast<std::int64_t>(
|
||||
static_cast<MakeSigned<decltype(value)>>(value));
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return result.as<std::int64_t>();
|
||||
if (empty() || size() != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
return makeSigned().visit([](auto value) -> std::optional<std::int64_t> {
|
||||
if constexpr (std::is_integral_v<typename decltype(value)::value_type>) {
|
||||
return static_cast<std::int64_t>(value[0]);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#define DEFINE_BINARY_OP(OP) \
|
||||
eval::Value eval::Value::operator OP(const Value &rhs) const { \
|
||||
using LhsCond = decltype([](auto &&lhs) { return true; }); \
|
||||
return visit<LhsCond>(*this, [&]<typename Lhs>(Lhs &&lhs) -> Value { \
|
||||
using RhsCond = decltype([](auto &&rhs) { \
|
||||
return requires(Lhs lhs) { static_cast<Value>(lhs OP rhs); }; \
|
||||
}); \
|
||||
return visit<RhsCond>(rhs, [&](auto &&rhs) -> Value { \
|
||||
return static_cast<Value>(lhs OP rhs); \
|
||||
if (index() != rhs.index() || size() != rhs.size()) { \
|
||||
return {}; \
|
||||
} \
|
||||
eval::Value result; \
|
||||
visit([&](auto lhsValues) { \
|
||||
rhs.visit([&](auto rhsValues) { \
|
||||
if constexpr (requires { lhsValues[0] OP rhsValues[0]; }) { \
|
||||
if constexpr (std::is_same_v<decltype(lhsValues[0]), \
|
||||
decltype(rhsValues[0])>) { \
|
||||
for (std::size_t i = 0; i < lhsValues.size(); ++i) { \
|
||||
result.add(lhsValues[i] OP rhsValues[i]); \
|
||||
} \
|
||||
} \
|
||||
} \
|
||||
}); \
|
||||
}); \
|
||||
return result; \
|
||||
}
|
||||
|
||||
#define DEFINE_UNARY_OP(OP) \
|
||||
eval::Value eval::Value::operator OP() const { \
|
||||
using Cond = decltype([](auto rhs) { \
|
||||
return requires { static_cast<Value>(OP rhs); }; \
|
||||
}); \
|
||||
return visit<Cond>(*this, [&](auto &&rhs) -> Value { \
|
||||
return static_cast<Value>(OP rhs); \
|
||||
eval::Value result; \
|
||||
visit([&](auto values) { \
|
||||
if constexpr (requires { OP values[0]; }) { \
|
||||
for (std::size_t i = 0; i < values.size(); ++i) { \
|
||||
result.add(OP values[i]); \
|
||||
} \
|
||||
} \
|
||||
}); \
|
||||
return result; \
|
||||
}
|
||||
|
||||
DEFINE_BINARY_OP(+);
|
||||
|
|
|
|||
Loading…
Reference in a new issue