/* -*- tab-width: 4; -*- */ /* vi: set sw=2 ts=4 expandtab: */ /* * Copyright 2023 The Khronos Group Inc. * SPDX-License-Identifier: Apache-2.0 */ /** * @internal * @file * @~English * * @brief Data Format Descriptor for imageio. * * @author Mark Callow */ #include #include #include #include #include /// @brief Image format descriptor /// /// Based on Khronos Data Format specification. Omits the parts needed for /// serialization (size, descriptorType, etc.) and, since there is no worry /// about writing & reading across compilers and platforms, uses a struct /// instead of the khr\_df.h macros. /// /// Note that @e samples are not @e channels, a.k.a @e components. @e samples /// represent a series of contiguous bits in the bitstream representing /// a pixel of the image. Since the various ImageInput derived classes /// convert incoming data to local endianness most @e channels need only /// a single sample. Only formats such as those with a shared exponent /// need multiple samples per component. /// /// This descriptor is way more general than is needed by the current set of /// supported input formats for which there will always be one sample per /// channel. We use this because it is familiar from use elsewhere in /// KTX-Software and because of the large number of useful enums provided /// by khr\_df.h. /// /// @note This class uses the Data Format Specification nomenclature of /// @e channel for consistency with thar spec. Elsewhere in KTX-Software /// @e component is widely used. /// struct FormatDescriptor { /// @internal /// @brief Basic descriptor. struct basicDescriptor { uint32_t colorModel: 8; uint32_t colorPrimaries: 8; uint32_t transferFunction: 8; uint32_t dataFlags: 8; uint32_t texelBlockDimension0: 8; uint32_t texelBlockDimension1: 8; uint32_t texelBlockDimension2: 8; uint32_t texelBlockDimension3: 8; uint32_t bytesPlane0: 8; uint32_t bytesPlane1: 8; uint32_t bytesPlane2: 8; uint32_t bytesPlane3: 8; uint32_t bytesPlane4: 8; uint32_t bytesPlane5: 8; uint32_t bytesPlane6: 8; uint32_t bytesPlane7: 8; /// @brief Default constructor basicDescriptor() { memset(this, 0, sizeof(*this)); } /// @brief Constructor for unpacked, non-compressed textures. basicDescriptor(uint32_t pixelByteCount, khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED, khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709, khr_df_model_e m = KHR_DF_MODEL_RGBSDA, khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT) { colorModel = m; colorPrimaries = p; transferFunction = t; dataFlags = f; texelBlockDimension0 = 0; // Uncompressed means only 1x1x1x1 blocks. texelBlockDimension1 = 0; texelBlockDimension2 = 0; texelBlockDimension3 = 0; bytesPlane0 = pixelByteCount; bytesPlane1 = bytesPlane2 = bytesPlane3 = 0; bytesPlane4 = bytesPlane5 = bytesPlane6 = bytesPlane7 = 0; } bool operator==(const basicDescriptor& rhs) const { const uint32_t* a = reinterpret_cast(this); const uint32_t* b = reinterpret_cast(&rhs); for (uint32_t i = 0; i < 4; i++) { if (a[i] != b[i]) return false; } return true; } bool operator!=(const basicDescriptor& rhs) const { return !(*this == rhs); } khr_df_model_e model() const noexcept { return static_cast(colorModel); } khr_df_primaries_e primaries() const noexcept { return static_cast(colorPrimaries); } khr_df_transfer_e transfer() const noexcept { return static_cast(transferFunction); } khr_df_flags_e flags() const noexcept { return static_cast(dataFlags); } } basic; /// @internal /// @brief Extended descriptor. /// /// In a true DFD this would be an extension descriptor type complete with /// size, vendorId, descriptorType, etc. struct extendedDescriptor { uint32_t channelCount; /// Saved channel count to avoid having to loop /// over samples to figure out the count. bool sameUnitAllChannels = false; /// All samples have same types and sizes. float oeGamma = -1; /// Power function exponent used when the image was /// encoded, if one was used. -1 otherwise. /// @internal /// @brief ICC profile descriptor. struct iccProfileDescriptor { std::string name; std::vector profile; iccProfileDescriptor() { } iccProfileDescriptor(std::string& n, uint8_t* p, size_t ps) { name = n; profile.resize(ps); profile.insert(profile.begin(), p, &p[ps]); } bool operator==(const iccProfileDescriptor& rhs) const { bool result = !this->name.compare(rhs.name); return result && (this->profile == rhs.profile); } bool operator!=(const iccProfileDescriptor& rhs) const { return !(*this == rhs); } } iccProfile; extendedDescriptor(uint32_t cc = 0) : channelCount(cc) { } bool operator==(const extendedDescriptor& rhs) const { if (this->channelCount != rhs.channelCount) return false; if (this->sameUnitAllChannels != rhs.sameUnitAllChannels) return false; if (this->oeGamma != rhs.oeGamma) return false; return this->iccProfile == rhs.iccProfile; } bool operator!=(const extendedDescriptor& rhs) const { return !(*this == rhs); } } extended; struct sample { uint32_t bitOffset: 16; uint32_t bitLength: 8; // uint32_t channelType: 8; uint32_t channelType: 4; uint32_t qualifierLinear: 1; uint32_t qualifierExponent: 1; uint32_t qualifierSigned: 1; uint32_t qualifierFloat: 1; uint32_t samplePosition0: 8; uint32_t samplePosition1: 8; uint32_t samplePosition2: 8; uint32_t samplePosition3: 8; uint32_t lower; uint32_t upper; bool operator==(const sample& rhs) const { const uint32_t* a = reinterpret_cast(this); const uint32_t* b = reinterpret_cast(&rhs); for (uint32_t i = 0; i < 4; i++) { if (a[i] != b[i]) return false; } return true; } /// @brief Constructs an uninitialized sample object sample() = default; /// @brief Construct a sample with default sampleUpper and sampleLower. /// /// For uncompressed formats. Handle integer data as normalized. For /// unsigned use the full range of the number of bits. For signed set /// sampleUpper and sampleLower so 0 is representable. sample(uint32_t chanType, uint32_t bLength, uint32_t offset, khr_df_sample_datatype_qualifiers_e dataType = static_cast(0), khr_df_transfer_e tf = KHR_DF_TRANSFER_UNSPECIFIED, khr_df_model_e m = KHR_DF_MODEL_RGBSDA) { bitOffset = offset; bitLength = bLength - 1; channelType = chanType; if (channelType == 3 && m != KHR_DF_MODEL_XYZW) { /// XYZW does not have an alpha chennel. *_ALPHA has the same /// value for all other 4-channel-capable uncompressed models. channelType = KHR_DF_CHANNEL_RGBSDA_ALPHA; } qualifierFloat = (dataType & KHR_DF_SAMPLE_DATATYPE_FLOAT) != 0; qualifierSigned = (dataType & KHR_DF_SAMPLE_DATATYPE_SIGNED) != 0; qualifierExponent = (dataType & KHR_DF_SAMPLE_DATATYPE_EXPONENT) != 0; qualifierLinear = (dataType & KHR_DF_SAMPLE_DATATYPE_LINEAR) != 0; if (tf > KHR_DF_TRANSFER_LINEAR && channelType == KHR_DF_CHANNEL_RGBSDA_ALPHA) { qualifierLinear = 1; } union { uint32_t i; float f; } uLower, uUpper; if (qualifierFloat) { if (qualifierSigned) { uUpper.f = 1.0f; uLower.f = -1.0f; } else { uUpper.f = 1.0f; uLower.f = 0.0f; } } else { if (qualifierSigned) { // signed normalized if (bitLength > 32) { uUpper.i = 0x7FFFFFFF; } else { uUpper.i = (1U << (bLength - 1)) - 1; } uLower.i = ~uUpper.i; uLower.i += 1; } else { // unsigned normalized if (bitLength > 32) { uUpper.i = 0xFFFFFFFFU; } else { uUpper.i = (uint32_t)((1U << bLength) - 1U); } uLower.i = 0U; } } lower = uLower.i; upper = uUpper.i; } /// @brief Construct a sample with custom sampleLower and sampleUpper. /// /// For uncompressed formats. sample(uint32_t chanType, uint32_t bitLength, uint32_t offset, uint32_t sampleLower, uint32_t sampleUpper, khr_df_sample_datatype_qualifiers_e dataType = static_cast(0), khr_df_transfer_e tf = KHR_DF_TRANSFER_UNSPECIFIED, khr_df_model_e m = KHR_DF_MODEL_RGBSDA) : sample(chanType, bitLength, offset, dataType, tf, m) { if (qualifierFloat) { throw std::runtime_error( "Invalid use of constructor for float data" ); } lower = sampleLower; upper = sampleUpper; } }; std::vector samples; /// @brief Default constructor /// /// Will have zero samples which means format unknown. FormatDescriptor() { } /// @brief Constructor for unpacked, non-compressed data. /// /// All channels have the same number of bits and basic data type. /// As all wide data types will be in local endianness we need only /// one sample per channel. /// /// Data is assumed to be unsigned normalized. @c sampleUpper will be /// set to the max value representable by @a channelBitLength. /// /// @c channelType will be set to the standard channel types for @a channelCount /// and @a m. FormatDescriptor(uint32_t channelCount, uint32_t channelBitLength, khr_df_sample_datatype_qualifiers_e dt = static_cast(0), khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED, khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709, khr_df_model_e m = KHR_DF_MODEL_RGBSDA, khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT) : basic((channelBitLength * channelCount) / 8, t, p, m, f), extended(channelCount) { for (uint32_t s = 0; s < channelCount; s++) { samples.push_back(sample(s, channelBitLength, s * channelBitLength, dt, t, m)); } if (m == KHR_DF_MODEL_YUVSDA && channelCount == 2) { samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA; } extended.sameUnitAllChannels = true; } /// @brief Constructor for unpacked, non-compressed data with custom /// sampleLower and sampleUpper /// /// All channels have the same number of bits and basic data type. /// Use this for unnormalized integer data or normalized data that does not /// use the full range representable by @a channelBitLength. FormatDescriptor(uint32_t channelCount, uint32_t channelBitLength, uint32_t sampleLower, uint32_t sampleUpper, khr_df_sample_datatype_qualifiers_e dt = static_cast(0), khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED, khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709, khr_df_model_e m = KHR_DF_MODEL_RGBSDA, khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT) : basic((channelBitLength * channelCount) / 8, t, p, m, f), extended(channelCount) { for (uint32_t s = 0; s < channelCount; s++) { samples.push_back(sample(s, channelBitLength, s * channelBitLength, sampleLower, sampleUpper, dt, t, m)); } if (m == KHR_DF_MODEL_YUVSDA && channelCount == 2) { samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA; } extended.sameUnitAllChannels = true; } static uint32_t totalBits(uint32_t sampleCount, std::vector& bits) { uint32_t totalBits = 0; for (uint32_t s = 0; s < sampleCount; s++) { totalBits += bits[s]; } return totalBits; } /// @brief Constructor for non-compressed textures with varying bit lengths or channel types. /// /// Each channel has the same basic data type. FormatDescriptor(uint32_t channelCount, std::vector& channelBitLengths, std::vector& channelTypes, khr_df_sample_datatype_qualifiers_e dt = static_cast(0), khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED, khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709, khr_df_model_e m = KHR_DF_MODEL_RGBSDA, khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT) : basic(totalBits(channelCount, channelBitLengths) >> 3, t, p, m, f), extended(channelCount) { if (channelCount > channelBitLengths.size() || channelCount > channelTypes.size()) { throw std::runtime_error( "Not enough channelBits or channelType specfications." ); } uint32_t bitOffset = 0; bool bitLengthsEqual = true; uint32_t firstBitLength = channelBitLengths[0]; for (uint32_t s = 0; s < channelCount; s++) { samples.push_back(sample(channelTypes[s], channelBitLengths[s], bitOffset, dt, t, m)); bitOffset += channelBitLengths[s]; if (firstBitLength != channelBitLengths[s]) { bitLengthsEqual = false; } } if (bitLengthsEqual) { extended.sameUnitAllChannels = true; } if (m == KHR_DF_MODEL_YUVSDA && channelCount == 2) { samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA; } } /// @brief Constructor for non-compressed textures with varying bit lengths /// or channel types and custom sampleLower and sampleUpper. /// /// Each channel has the same basic data type. Use this for unnormalized /// integer data or normalized data that does not use the full bit range. FormatDescriptor(uint32_t channelCount, std::vector& channelBitLengths, std::vector& channelTypes, std::vector& samplesLower, std::vector& samplesUpper, khr_df_sample_datatype_qualifiers_e dt = static_cast(0), khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED, khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709, khr_df_model_e m = KHR_DF_MODEL_RGBSDA, khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT) : basic(totalBits(channelCount, channelBitLengths) >> 3, t, p, m, f), extended(channelCount) { if (channelCount > channelBitLengths.size() || channelCount > channelTypes.size()) { throw std::runtime_error( "Not enough channelBits or channelType specfications." ); } uint32_t bitOffset = 0; bool bitLengthsEqual = true; uint32_t firstBitLength = channelBitLengths[0]; for (uint32_t s = 0; s < channelCount; s++) { samples.push_back(sample(channelTypes[s], channelBitLengths[s], samplesLower[s], samplesUpper[s], bitOffset, dt, t, m)); bitOffset += channelBitLengths[s]; if (firstBitLength != channelBitLengths[s]) { bitLengthsEqual = false; } } if (bitLengthsEqual) { extended.sameUnitAllChannels = true; } if (m == KHR_DF_MODEL_YUVSDA && channelCount == 2) { samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA; } } /// @brief Constructor for non-compressed, shared exponent format. /// /// Each channel is a floating point. All channels share the same exponent /// and have the same number of mantissa bits. // TODO: Handle whether there is an implicit 1 and a sign bit. FormatDescriptor(uint32_t channelCount, uint32_t mantissaBitLength, uint32_t exponentBitLength, khr_df_sample_datatype_qualifiers_e dt = static_cast(0), khr_df_transfer_e t = KHR_DF_TRANSFER_LINEAR, khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709, khr_df_model_e m = KHR_DF_MODEL_RGBSDA, khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT) : basic((channelCount * mantissaBitLength + exponentBitLength) >> 3, t, p, m, f), extended(channelCount) { if (dt & KHR_DF_SAMPLE_DATATYPE_FLOAT) { throw std::runtime_error( "DATATYPE_FLOAT is set for a shared exponent format"); } for (uint32_t s = 0; s < channelCount; s++) { uint32_t sampleLower = 0, sampleUpper; // sampleUpper and sampleLower values for the mantissa should be // set to indicate the representation of 1.0 and 0.0 (for unsigned // formats) or -1.0 (for signed formats) respectively when the // exponent is in a 0 position after any bias has been corrected. // If there is an implicit 1 bit, these values for the mantissa // will exceed what can be represented in the number of available // mantissa bits. sampleUpper = 1U << mantissaBitLength; samples.push_back(sample(s, mantissaBitLength, s * mantissaBitLength, sampleLower, sampleUpper, dt, t, m)); // The sampleLower for the exponent should indicate the exponent // bias. That is, the mantissa should be scaled by two raised to // the power of the stored exponent minus this sampleLower value. // // The sampleUpper for the exponent indicates the maximum legal // exponent value. Values above this are used to encode infinities // and not-a-number (NaN) values. sampleUpper can therefore be used // to indicate whether or not the format supports these encodings. //sampleLower = exponentBias; //sampleUpper = maxLegalExponentValue; samples.push_back(sample(s, exponentBitLength, channelCount * mantissaBitLength, sampleLower, sampleUpper, static_cast(dt | KHR_DF_SAMPLE_DATATYPE_EXPONENT), t, m)); } extended.sameUnitAllChannels = true; } /// @brief Constructor from pre-constructed basic and sample descriptors FormatDescriptor( FormatDescriptor::basicDescriptor basic, std::vector samples_) : basic(basic), extended(static_cast(samples_.size())), samples(std::move(samples_)) { extended.sameUnitAllChannels = true; if (!samples.empty()) { for (uint32_t i = 1; i < static_cast(samples.size()); ++i) { if (samples[0].bitLength != samples[i].bitLength || samples[0].qualifierLinear != samples[i].qualifierLinear || samples[0].qualifierExponent != samples[i].qualifierExponent || samples[0].qualifierSigned != samples[i].qualifierSigned || samples[0].qualifierFloat != samples[i].qualifierFloat) { extended.sameUnitAllChannels = false; break; } } } } bool isUnknown() const noexcept { return samples.size() == 0; } bool sameUnitAllChannels() const noexcept { return extended.sameUnitAllChannels; } bool operator==(const FormatDescriptor& rhs) const { if (this->basic != rhs.basic) return false; if (this->extended != rhs.extended) return false; return this->samples == rhs.samples; } bool operator!=(const FormatDescriptor& rhs) const { return !(*this == rhs); } khr_df_model_e model() const noexcept { return basic.model(); } khr_df_primaries_e primaries() const noexcept { return basic.primaries(); } khr_df_transfer_e transfer() const noexcept { return basic.transfer(); } khr_df_flags_e flags() const noexcept { return basic.flags(); } float oeGamma() const noexcept { return extended.oeGamma; } const std::string& iccProfileName() const noexcept { return extended.iccProfile.name; } const std::vector& iccProfile() const noexcept { return extended.iccProfile.profile; } void setModel(khr_df_model_e m) { basic.colorModel = m; } void setPrimaries(khr_df_primaries_e p) { basic.colorPrimaries = p; } void setTransfer(khr_df_transfer_e t) { khr_df_transfer_e oldOetf = transfer(); basic.transferFunction = t; if ((oldOetf <= KHR_DF_TRANSFER_LINEAR) != (t <= KHR_DF_TRANSFER_LINEAR)) { std::vector::iterator sit = samples.begin(); for (; sit < samples.end(); sit++) { if (sit->channelType == KHR_DF_CHANNEL_RGBSDA_ALPHA) { sit->qualifierLinear = t > KHR_DF_TRANSFER_LINEAR; } } } } uint32_t pixelByteCount() const noexcept { return basic.bytesPlane0; } uint32_t sampleCount() const noexcept { return static_cast(samples.size()); } uint32_t sampleBitLength(uint32_t s) const noexcept { return samples[s].bitLength + 1; } // TODO: remove? uint32_t sampleByteCount(uint32_t s) const noexcept { // Use integer division so 0 is returned when length is < a byte. return sampleBitLength(s) / 8; } uint32_t sampleUpper(uint32_t s) const noexcept { return samples[s].upper; } uint32_t channelCount() const noexcept { return extended.channelCount; } uint32_t channelBitLength(khr_df_model_channels_e c) const { std::vector::const_iterator it = samples.begin(); uint32_t bitLength = 0; for (; it < samples.end(); it++) { if (it->channelType == static_cast(c)) { bitLength += it->bitLength + 1; } } if (bitLength == 0) { throw std::runtime_error("No such channel."); } return bitLength; } uint32_t channelBitLength() const { if (channelCount() == 1) return samples[0].bitLength + 1; if (!extended.sameUnitAllChannels) { throw std::runtime_error( "Differing size channels. Specify channel to query." ); } return channelBitLength(KHR_DF_CHANNEL_RGBSDA_R); } uint32_t largestChannelBitLength() const { uint32_t maxBitLength = 0; for (uint32_t i = 0; i < 16; ++i) { uint32_t bitLength = 0; for (const auto& sample : samples) if (sample.channelType == i) bitLength += sample.bitLength + 1; if (bitLength > maxBitLength) maxBitLength = bitLength; } return maxBitLength; } bool anyChannelBitLengthNotEqual(uint32_t bitLength) const { for (uint32_t i = 0; i < 16; ++i) { uint32_t channelBitLength = 0; for (const auto& sample : samples) if (sample.channelType == i) channelBitLength += sample.bitLength + 1; if (bitLength != channelBitLength) return true; } return false; } khr_df_sample_datatype_qualifiers_e channelDataType(khr_df_model_channels_e c) const { // TODO: Fix for shared exponent case... std::vector::const_iterator it = samples.begin(); for (; it < samples.end(); it++) { if (it->channelType == static_cast(c)) { return static_cast (it->channelType & KHR_DF_SAMPLEMASK_QUALIFIERS); } } throw std::runtime_error("No such channel."); } khr_df_sample_datatype_qualifiers_e channelDataType() const { if (!extended.sameUnitAllChannels) { throw std::runtime_error( "Differing size channels. Specify channel to query." ); } return channelDataType(KHR_DF_CHANNEL_RGBSDA_R); } uint32_t channelUpper() const { if (extended.channelCount != samples.size()) { throw std::runtime_error( "Multiple samples per channel. Call sampleUpper(uint32_t s)." ); } if (!extended.sameUnitAllChannels) { throw std::runtime_error( "Differing size channels. Call sampleUpper(uint32_t s)." ); } return sampleUpper(KHR_DF_CHANNEL_RGBSDA_R); } void updateSampleInfo(uint32_t channelCount, uint32_t channelBitLength, uint32_t sampleLower, uint32_t sampleUpper, khr_df_sample_datatype_qualifiers_e dt = static_cast(0)) { samples.clear(); for (uint32_t s = 0; s < channelCount; s++) { samples.push_back(sample(s, channelBitLength, s * channelBitLength, sampleLower, sampleUpper, dt, basic.transfer(), basic.model())); } if (basic.model() == KHR_DF_MODEL_YUVSDA && channelCount == 2) { samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA; } extended.channelCount = channelCount; extended.sameUnitAllChannels = true; } void updateSampleBitCounts(std::vector& bits) { uint32_t b, s; uint32_t offset = 0; for (b = 0, s = 0; s < samples.size(); s++) { samples[s].bitLength = bits[b] - 1; samples[s].bitOffset = offset; offset += bits[b]; if (b < bits.size() - 1) b++; // else set remaining sample sizes to last available bits value. } } void updateChannelCount(uint32_t newCount) { if (newCount == extended.channelCount) return; if (extended.channelCount != samples.size()) { // TODO: Either fix error handling or implement throw std::runtime_error( "changeChannelCount not supported when # samples != # channels"); } extended.channelCount = newCount; if (newCount < samples.size()) { samples.erase(samples.begin() + newCount, samples.end()); return; } uint32_t firstNewIndex = static_cast(samples.size()); uint32_t offset = samples.back().bitOffset + samples.back().bitLength + 1; samples.resize(newCount, samples.back()); std::vector::iterator sit = samples.begin() + firstNewIndex; for (; sit < samples.end(); sit++) { sit->bitOffset = offset; offset += (sit->bitLength + 1); } } [[nodiscard]] const sample* find(khr_df_model_channels_e channel) const { for (const auto& sample : samples) if (sample.channelType == static_cast(channel)) return &sample; return nullptr; } void removeLastChannel() { const auto numChannels = static_cast(samples.size()); assert(numChannels > 1); assert(basic.bytesPlane0 % numChannels == 0); samples.pop_back(); basic.bytesPlane0 = basic.bytesPlane0 / numChannels * (numChannels - 1u); if (extended.channelCount != 0) --extended.channelCount; } friend std::ostream& operator<< (std::ostream& o, khr_df_sample_datatype_qualifiers_e q) { if (q & KHR_DF_SAMPLE_DATATYPE_SIGNED) o << " signed "; if (q & KHR_DF_SAMPLE_DATATYPE_FLOAT) { if (!(q & KHR_DF_SAMPLE_DATATYPE_SIGNED)) o << " "; o << "float "; } return o; } };