From 334567d33bbf0cf4405ab9c5b8cd72ba9450e575 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Tue, 11 Jul 2023 01:42:15 -0400 Subject: [PATCH] common: Introduce macro-based BitField The current BitField implementation requires the use of the union, which makes it both non-constexpr (due to accessing inactive union members), and prevents the usage of smaller types and enums, reducing its flexibility. Furthermore, BitField is not trivially copyable (and by extension, not trivial). Moreover, it is not well documented that BitField performs automatic sign extension on signed integers, as a recent PR made an assumption that these signed integers were zero extended. These considerations resulted in this new BitField implementation, which uses macros to generate functions for accessing / setting bitfield values. 2 macros are provided: `YUZU_RO_BITFIELD` for read-only BitFields, which generates a member function that directly returns the specified type. `YUZU_BITFIELD`, which generates a member function that returns a `BitFieldHelper` class which allows setting the value using `operator=` or `Set()`, and performs implicit conversion to the specified type. Types: The following type tags are added: Automatic type deduction: AutoUInt, AutoSignExtSInt, AutoZeroExtSInt, AutoFloat Automatic type deduction selects the smallest type that contains at least the specified NumBits bits. However, in testing, MSVC is pretty bad at optimizing smaller-than-32-bit accesses, so it may be advisable to specify 32 bit types in performance sensitive code. Signed Integers: SignExtSInt<>, ZeroExtSInt<> The new SignExtSInt and ZeroExtSInt are mandatory type tags for signed integers to explicitly specify whether to sign extend (preserving the sign and value) or zero extend the bitfield value. These changes have sacrificed compatibility with `_be` types. This is a worthwhile sacrifice as we do not have usages of these types in BitField. Example usage: ```cpp struct MyTestStruct { u32 raw; YUZU_RO_BITFIELD(0, 3, AutoUInt, field0); YUZU_BITFIELD(3, 4, AutoSignExtSInt, field1); YUZU_BITFIELD(7, 5, ZeroExtSInt, field2); YUZU_RO_BITFIELD(12, 1, bool, flag0); }; MyTestStruct s{0x1234567}; s.field1() = -1; s.field2() = -2; fmt::print("field0={}, field1={}, field2={}, flag0={}", s.field0(), s.field1(), s.field2(), s.flag0()); ``` --- src/common/CMakeLists.txt | 1 + src/common/new_bit_field.h | 398 +++++++++++++++++++++++++++++ src/tests/CMakeLists.txt | 1 + src/tests/common/new_bit_field.cpp | 216 ++++++++++++++++ 4 files changed, 616 insertions(+) create mode 100644 src/common/new_bit_field.h create mode 100644 src/tests/common/new_bit_field.cpp diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 3adf13a3f4..1c92ae8a80 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -90,6 +90,7 @@ add_library(common STATIC microprofileui.h multi_level_page_table.cpp multi_level_page_table.h + new_bit_field.h nvidia_flags.cpp nvidia_flags.h overflow.h diff --git a/src/common/new_bit_field.h b/src/common/new_bit_field.h new file mode 100644 index 0000000000..30acaeb288 --- /dev/null +++ b/src/common/new_bit_field.h @@ -0,0 +1,398 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#include "common/bit_cast.h" + +// Forward declarations + +struct AutoUInt; +struct AutoSignExtSInt; +struct AutoZeroExtSInt; +struct AutoFloat; +template +struct SignExtSInt; +template +struct ZeroExtSInt; + +namespace Common::BitField { + +template +constexpr size_t BitSize() { + return sizeof(T) * 8; +} + +template +concept IsPOD = std::is_trivial_v && std::is_standard_layout_v; + +template ::type> +struct ToUnderlyingType { + using type = T; +}; + +template +struct ToUnderlyingType { + using type = std::underlying_type_t; +}; + +// clang-format off + +// Automatically deduces the smallest unsigned integer type that contains at least NumBits bits. +template +using SmallestUIntType = std::conditional_t(), std::uint8_t, + std::conditional_t(), std::uint16_t, + std::conditional_t(), std::uint32_t, + std::conditional_t(), std::uint64_t, std::uint64_t>>>>; + +// Automatically deduces the smallest signed integer type that contains at least NumBits bits. +template +using SmallestSIntType = std::conditional_t(), std::int8_t, + std::conditional_t(), std::int16_t, + std::conditional_t(), std::int32_t, + std::conditional_t(), std::int64_t, std::int64_t>>>>; + +// Automatically deduces the smallest floating point type that contains at exactly NumBits bits. +template +using SmallestFloatType = std::conditional_t(), float, + std::conditional_t(), double, double>>; + +// clang-format on + +struct TagType {}; + +struct AutoType : public TagType {}; + +struct SIntType : public TagType {}; + +template ::type> +struct DeduceAutoType { + using type = void; +}; + +template +struct DeduceAutoType { + // clang-format off + using type = std::conditional_t, Common::BitField::SmallestUIntType, + std::conditional_t, Common::BitField::SmallestSIntType, + std::conditional_t, Common::BitField::SmallestSIntType, + std::conditional_t, Common::BitField::SmallestFloatType, void>>>>; + // clang-format on +}; + +template ::type> +struct DeduceSIntType { + using type = void; +}; + +template +struct DeduceSIntType { + using type = typename T::OutputType; +}; + +template ::type> +struct DeduceSignExtendValue { + static constexpr bool value = false; +}; + +template +struct DeduceSignExtendValue { + static constexpr bool value = T::SignExtend; +}; + +template +struct TypeTraits { + static_assert(NumBits != 0, "NumBits must not be 0."); + static_assert(!std::is_same_v || NumBits == 1, "For bool, NumBits must be exactly 1."); + static_assert(!std::signed_integral::type>, + "For signed integers, use SignExtSInt or ZeroExtSInt instead."); + + using AutoType = typename DeduceAutoType::type; + using SIntType = typename DeduceSIntType::type; + + // clang-format off + using OutputType = std::conditional_t, AutoType, + std::conditional_t, SIntType, + T>>; + // clang-format on + + static_assert(IsPOD, "Type must be a POD type."); + + using UnderlyingType = typename ToUnderlyingType::type; + + // For integral types, assert that OutputType contains at least NumBits bits. + static_assert(!std::is_integral_v || BitSize() >= NumBits, + "Type must contain at least NumBits bits."); + // For all other types, assert that OutputType contains exactly NumBits bits. + static_assert(std::is_integral_v || BitSize() == NumBits, + "Type must contain exactly NumBits bits."); + + static constexpr bool SignExtend = DeduceSignExtendValue::value; +}; + +template +constexpr To AutoBitCast(const From& from) { + if constexpr (sizeof(To) != sizeof(From)) { + using FromUnsignedType = SmallestUIntType()>; + using ToUnsignedType = SmallestUIntType()>; + return BitCast(static_cast(BitCast(from))); + } else { + return BitCast(from); + } +} + +template +constexpr UnsignedRawType GenerateBitMask() { + static_assert(std::unsigned_integral); + if constexpr (BitSize() == NumBits) { + return ~UnsignedRawType{0}; + } else { + return ((UnsignedRawType{1} << NumBits) - 1) << Position; + } +} + +template +constexpr void InsertBits(RawType& raw, OutputType value) { + using UnsignedType = SmallestUIntType()>; + static_assert(sizeof(RawType) == sizeof(UnsignedType)); + constexpr auto Mask = GenerateBitMask(); + raw = AutoBitCast((AutoBitCast(raw) & ~Mask) | + ((AutoBitCast(value) << Position) & Mask)); +} + +template +constexpr OutputType ExtractBits(const RawType& raw) { + if constexpr (SignExtend) { + using SignedType = SmallestSIntType()>; + static_assert(sizeof(RawType) == sizeof(SignedType)); + constexpr auto RightShift = BitSize() - NumBits; + constexpr auto LeftShift = RightShift - Position; + return AutoBitCast((AutoBitCast(raw) << LeftShift) >> RightShift); + } else { + using UnsignedType = SmallestUIntType()>; + static_assert(sizeof(RawType) == sizeof(UnsignedType)); + constexpr auto Mask = GenerateBitMask(); + return AutoBitCast((AutoBitCast(raw) & Mask) >> Position); + } +} + +template +class BitFieldHelper final { +public: + constexpr BitFieldHelper(RawType& raw_) : raw{raw_} {} + + BitFieldHelper(const BitFieldHelper&) = delete; + BitFieldHelper& operator=(const BitFieldHelper&) = delete; + + BitFieldHelper(BitFieldHelper&&) = delete; + BitFieldHelper& operator=(BitFieldHelper&&) = delete; + + constexpr void Set(OutputType value) noexcept { + return InsertBits(raw, value); + } + + constexpr BitFieldHelper& operator=(OutputType value) noexcept { + Set(value); + return *this; + } + + [[nodiscard]] constexpr OutputType Get() const noexcept { + return ExtractBits(raw); + } + + [[nodiscard]] constexpr operator OutputType() const noexcept { + return Get(); + } + + template + [[nodiscard]] friend constexpr bool operator==(const BitFieldHelper& lhs, + const Other& rhs) noexcept { + return lhs.Get() == rhs; + } + + template + [[nodiscard]] friend constexpr bool operator==(const Other& lhs, + const BitFieldHelper& rhs) noexcept { + return lhs == rhs.Get(); + } + + [[nodiscard]] constexpr bool operator==(const BitFieldHelper& other) const noexcept { + return Get() == other.Get(); + } + +private: + RawType& raw; +}; + +template +inline auto format_as(BitFieldHelper bitfield) { + return bitfield.Get(); +} + +} // namespace Common::BitField + +/** + * A type tag that automatically deduces the smallest unsigned integer type that + * contains at least NumBits bits in the bitfield. + * Currently supported unsigned integer types: + * - u8 (8 bits) + * - u16 (16 bits) + * - u32 (32 bits) + * - u64 (64 bits) + */ +struct AutoUInt : public Common::BitField::AutoType { + static constexpr bool SignExtend = false; +}; + +/** + * A type tag that automatically deduces the smallest signed integer type that + * contains at least NumBits bits in the bitfield. + * Additionally, the value is treated as a NumBits-bit 2's complement integer + * which is sign-extended to fit in the output type, preserving its sign and value. + * Currently supported signed integer types: + * - s8 (8 bits) + * - s16 (16 bits) + * - s32 (32 bits) + * - s64 (64 bits) + */ +struct AutoSignExtSInt : public Common::BitField::AutoType { + static constexpr bool SignExtend = true; +}; + +/** + * A type tag that automatically deduces the smallest signed integer type that + * contains at least NumBits bits in the bitfield. + * Unlike AutoSignExtInt, the value is zero-extended to fit in the output type, + * effectively treating it as an unsigned integer that is bitcast to a signed integer. + * Its sign and value are not preserved, unless the output type contains exactly NumBits bits. + * Currently supported signed integer types: + * - s8 (8 bits) + * - s16 (16 bits) + * - s32 (32 bits) + * - s64 (64 bits) + */ +struct AutoZeroExtSInt : public Common::BitField::AutoType { + static constexpr bool SignExtend = false; +}; + +/** + * A type tag that automatically deduces the smallest floating point type that + * contains exactly NumBits bits in the bitfield. + * Currently supported floating point types: + * - float (32 bits) + * - double (64 bits) + */ +struct AutoFloat : public Common::BitField::AutoType { + static constexpr bool SignExtend = false; +}; + +/** + * A type tag that treats the value as a NumBits-bit 2's Complement Integer + * which is sign-extended to fit in the output type, preserving its sign and value. + * Currently supported signed integer types: + * - s8 (8 bits) + * - s16 (16 bits) + * - s32 (32 bits) + * - s64 (64 bits) + */ +template +struct SignExtSInt : public Common::BitField::SIntType { + static_assert(std::signed_integral::type>, + "SignExtSInt: Type must be a signed integral."); + + using OutputType = T; + static constexpr bool SignExtend = true; +}; + +/** + * A type tag that, unlike SignExtInt, zero-extends the value to fit in the output type, + * effectively treating it as an unsigned integer that is bitcast to a signed integer. + * Its sign and value are not preserved, unless the output type contains exactly NumBits bits. + * Currently supported signed integer types: + * - s8 (8 bits) + * - s16 (16 bits) + * - s32 (32 bits) + * - s64 (64 bits) + */ +template +struct ZeroExtSInt : public Common::BitField::SIntType { + static_assert(std::signed_integral::type>, + "ZeroExtSInt: Type must be a signed integral."); + + using OutputType = T; + static constexpr bool SignExtend = false; +}; + +template +using BitFieldOutputType = typename Common::BitField::TypeTraits::OutputType; + +#define YUZU_RO_BITFIELD(Position, NumBits, Type, Name) \ + constexpr BitFieldOutputType Name() const { \ + using BitFieldTypeTraits = Common::BitField::TypeTraits; \ + using OutputType = BitFieldTypeTraits::OutputType; \ + constexpr bool SignExtend = BitFieldTypeTraits::SignExtend; \ + using ThisType = std::remove_cvref_t; \ + static_assert(!std::is_union_v, \ + "An object containing BitFields cannot be a union type."); \ + static_assert(Common::BitField::IsPOD, \ + "An object containing BitFields must be a POD type."); \ + static_assert(Common::BitField::BitSize() <= 64, \ + "An object containing BitFields must be at most 64 bits in size."); \ + /* A structured binding is used to decompose *this into its constituent members. */ \ + /* It also allows us to guarantee that we only have one member in *this object. */ \ + /* Bit manipulation is performed on this member, so it must support bit operators. */ \ + const auto& [yuzu_raw_value] = *this; \ + using RawType = std::remove_cvref_t; \ + static_assert(Common::BitField::IsPOD, \ + "An object containing BitFields must be a POD type."); \ + static_assert(Common::BitField::BitSize() <= 64, \ + "An object containing BitFields must be at most 64 bits in size."); \ + static_assert(Position < Common::BitField::BitSize(), \ + "BitField is out of range."); \ + static_assert(NumBits <= Common::BitField::BitSize(), \ + "BitField is out of range."); \ + static_assert(Position + NumBits <= Common::BitField::BitSize(), \ + "BitField is out of range."); \ + return Common::BitField::ExtractBits( \ + yuzu_raw_value); \ + } + +#define YUZU_BITFIELD(Position, NumBits, Type, Name) \ + YUZU_RO_BITFIELD(Position, NumBits, Type, Name); \ + constexpr auto Name() { \ + using BitFieldTypeTraits = Common::BitField::TypeTraits; \ + using OutputType = BitFieldTypeTraits::OutputType; \ + constexpr bool SignExtend = BitFieldTypeTraits::SignExtend; \ + using ThisType = std::remove_cvref_t; \ + static_assert(!std::is_union_v, \ + "An object containing BitFields cannot be a union type."); \ + static_assert(Common::BitField::IsPOD, \ + "An object containing BitFields must be a POD type."); \ + static_assert(Common::BitField::BitSize() <= 64, \ + "An object containing BitFields must be at most 64 bits in size."); \ + /* A structured binding is used to decompose *this into its constituent members. */ \ + /* It also allows us to guarantee that we only have one member in *this object. */ \ + /* Bit manipulation is performed on this member, so it must support bit operators. */ \ + auto& [yuzu_raw_value] = *this; \ + using RawType = std::remove_cvref_t; \ + static_assert(Common::BitField::IsPOD, \ + "An object containing BitFields must be a POD type."); \ + static_assert(Common::BitField::BitSize() <= 64, \ + "An object containing BitFields must be at most 64 bits in size."); \ + static_assert(Position < Common::BitField::BitSize(), \ + "BitField is out of range."); \ + static_assert(NumBits <= Common::BitField::BitSize(), \ + "BitField is out of range."); \ + static_assert(Position + NumBits <= Common::BitField::BitSize(), \ + "BitField is out of range."); \ + return Common::BitField::BitFieldHelper{yuzu_raw_value}; \ + } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 1e158f3759..8485d96417 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(tests common/container_hash.cpp common/fibers.cpp common/host_memory.cpp + common/new_bit_field.cpp common/param_package.cpp common/range_map.cpp common/ring_buffer.cpp diff --git a/src/tests/common/new_bit_field.cpp b/src/tests/common/new_bit_field.cpp new file mode 100644 index 0000000000..7f5e49d30f --- /dev/null +++ b/src/tests/common/new_bit_field.cpp @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/new_bit_field.h" + +namespace Common::BitField { + +enum class U8Enum : std::uint8_t {}; +enum class U16Enum : std::uint16_t {}; +enum class U32Enum : std::uint32_t {}; +enum class U64Enum : std::uint64_t {}; + +enum class S8Enum : std::int8_t {}; +enum class S16Enum : std::int16_t {}; +enum class S32Enum : std::int32_t {}; +enum class S64Enum : std::int64_t {}; + +template +struct NByteStruct { + std::array raw; +}; + +struct NonTrivialStruct { + NonTrivialStruct() {} +}; + +struct NonTriviallyCopyableStruct { + NonTriviallyCopyableStruct(const NonTriviallyCopyableStruct&) {} +}; + +struct NonStandardLayoutStruct { + std::uint8_t a; + +private: + std::uint8_t b; +}; + +struct NonPODStruct { + virtual void Foo() const = 0; +}; + +// clang-format off + +// Tests that must pass. + +static_assert(std::is_same_v>); + +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); + +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 8>>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 8>>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v, 9>>); +static_assert(std::is_same_v, 16>>); +static_assert(std::is_same_v, 9>>); +static_assert(std::is_same_v, 16>>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v, 17>>); +static_assert(std::is_same_v, 32>>); +static_assert(std::is_same_v, 17>>); +static_assert(std::is_same_v, 32>>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v, 33>>); +static_assert(std::is_same_v, 64>>); +static_assert(std::is_same_v, 33>>); +static_assert(std::is_same_v, 64>>); + +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); + +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); +static_assert(std::is_same_v>); + +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 8>>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 8>>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 16>>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 16>>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 32>>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 32>>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 64>>); +static_assert(std::is_same_v, 1>>); +static_assert(std::is_same_v, 64>>); + +static_assert(std::is_same_v, BitFieldOutputType, 8>>); +static_assert(std::is_same_v, BitFieldOutputType, 16>>); +static_assert(std::is_same_v, BitFieldOutputType, 24>>); +static_assert(std::is_same_v, BitFieldOutputType, 32>>); +static_assert(std::is_same_v, BitFieldOutputType, 40>>); +static_assert(std::is_same_v, BitFieldOutputType, 48>>); +static_assert(std::is_same_v, BitFieldOutputType, 56>>); +static_assert(std::is_same_v, BitFieldOutputType, 64>>); + +static_assert(TypeTraits::SignExtend == true); +static_assert(TypeTraits::SignExtend == false); +static_assert(TypeTraits, 8>::SignExtend == true); +static_assert(TypeTraits, 8>::SignExtend == true); +static_assert(TypeTraits, 8>::SignExtend == false); +static_assert(TypeTraits, 8>::SignExtend == false); + +// Tests that will fail if uncommented. + +// NumBits must not be 0. + +// static_assert(std::is_same_v>); + +// For bool, NumBits must be exactly 1. + +// static_assert(std::is_same_v>); + +// For signed integers, use SignExtSInt or ZeroExtSInt instead. + +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); + +// SignExtSInt: Type must be a signed integral. + +// static_assert(std::is_same_v, 8>>); +// static_assert(std::is_same_v, 32>>); +// static_assert(std::is_same_v, 8>>); +// static_assert(std::is_same_v, BitFieldOutputType>, 8>>); + +// ZeroExtSInt: Type must be a signed integral. + +// static_assert(std::is_same_v, 8>>); +// static_assert(std::is_same_v, 32>>); +// static_assert(std::is_same_v, 8>>); +// static_assert(std::is_same_v, BitFieldOutputType>, 8>>); + +// Type must be a POD type. + +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); + +// Type must contain at least NumBits bits. + +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); + +// Type must contain exactly NumBits bits. + +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v>); +// static_assert(std::is_same_v, BitFieldOutputType, 7>>); +// static_assert(std::is_same_v, BitFieldOutputType, 15>>); +// static_assert(std::is_same_v, BitFieldOutputType, 23>>); +// static_assert(std::is_same_v, BitFieldOutputType, 31>>); +// static_assert(std::is_same_v, BitFieldOutputType, 39>>); +// static_assert(std::is_same_v, BitFieldOutputType, 47>>); +// static_assert(std::is_same_v, BitFieldOutputType, 55>>); +// static_assert(std::is_same_v, BitFieldOutputType, 63>>); + +// clang-format on + +} // namespace Common::BitField