blob: 3698404ec10ab491fe68e97de10c2f93cf6b09b3 [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkCombinationBuilder.h"
#include "src/core/SkKeyContext.h"
#include "src/core/SkKeyHelpers.h"
#include "src/core/SkMathPriv.h"
#include "src/core/SkShaderCodeDictionary.h"
#ifdef SK_GRAPHITE_ENABLED
#include "src/gpu/graphite/ContextPriv.h"
#endif
#define SHADER_TYPES(M) \
M(SolidColor) \
\
M(LinearGradient) \
M(RadialGradient) \
M(SweepGradient) \
M(ConicalGradient) \
\
M(LocalMatrix) \
M(Image) \
M(BlendShader)
// To keep the SHADER_TYPES list and SkShaderType aligned, we create a hidden enum class
// and check it against SkShaderType
#define MAKE_ENUM(T) k##T,
enum class UnusedShaderType { SHADER_TYPES(MAKE_ENUM) };
#undef MAKE_ENUM
static constexpr int kUnusedShaderTypeCount = static_cast<int>(UnusedShaderType::kBlendShader) + 1;
#define STATIC_ASSERT(T) static_assert((int) SkShaderType::k##T == (int) UnusedShaderType::k##T);
SHADER_TYPES(STATIC_ASSERT)
#undef STATIC_ASSERT
static_assert(kSkShaderTypeCount == kUnusedShaderTypeCount);
//--------------------------------------------------------------------------------------------------
namespace {
int tm_pair_to_index(SkTileModePair tileModes) {
static_assert(kSkTileModeCount <= 4);
return (int) tileModes.fX << 2 | (int) tileModes.fY;
}
#ifdef SK_DEBUG
SkTileModePair index_to_tm_pair(int index) {
static_assert(kSkTileModeCount <= 4);
SkASSERT(index < 16);
int tmX = index >> 2;
int tmY = index & 3;
return { (SkTileMode) tmX, (SkTileMode) tmY };
}
void add_indent(int indent) {
SkDebugf("%*c", indent, ' ');
}
const char* type_to_str(SkShaderType type) {
switch (type) {
#define CASE(T) case SkShaderType::k##T: return #T;
SHADER_TYPES(CASE)
#undef CASE
}
SkUNREACHABLE;
}
#endif // SK_DEBUG
} // anonymous namespace
//--------------------------------------------------------------------------------------------------
// The base class for all the objects stored in the arena
// The base option object consists of some number of child slots each of which is an array of
// options. The slots and their associated linked list of options are allocated separately in
// the arena.
class SkOption {
public:
// The SkSlot holds the combinatorial options for a single child slot of an
// option (in a linked list).
class SkSlot {
public:
void addOption(SkOption* newOption);
int numCombinations() const;
void addToKey(const SkKeyContext&, int desiredCombination, SkPaintParamsKeyBuilder*);
#ifdef SK_DEBUG
void dump(int indent) const;
#endif
private:
SkOption* fHead = nullptr;
SkOption* fTail = nullptr;
};
SkOption(SkShaderType type, int numSlots)
: fType(type)
, fNumSlots(numSlots) {}
SkShaderType type() const { return fType; }
int numSlots() const { return fNumSlots; }
#ifdef SK_DEBUG
int epoch() const { return fEpoch; }
void setEpoch(int epoch) {
SkASSERT(fEpoch == kInvalidEpoch && epoch != kInvalidEpoch);
fEpoch = epoch;
}
#endif
void setSlotsArray(SkSlot* slots) {
SkASSERT(!fSlots);
fSlots = slots;
}
SkSlot* getSlot(int slotIndex) {
if (slotIndex >= fNumSlots) {
return nullptr;
}
return &fSlots[slotIndex];
}
void addOption(int slotIndex, SkOption* newOption) {
if (slotIndex >= fNumSlots) {
return;
}
SkSlot* slot = this->getSlot(slotIndex);
slot->addOption(newOption);
}
int numChildCombinations() const {
int numChildCombinations = 1;
for (int i = 0; i < fNumSlots; ++i) {
numChildCombinations *= fSlots[i].numCombinations();
}
return numChildCombinations;
}
int numCombinations() const {
return this->numIntrinsicCombinations() * this->numChildCombinations();
}
void addToKey(const SkKeyContext& keyContext,
int desiredCombination,
SkPaintParamsKeyBuilder* keyBuilder) {
SkASSERT(desiredCombination < this->numCombinations());
int intrinsicCombination = desiredCombination / this->numChildCombinations();
int childCombination = desiredCombination % this->numChildCombinations();
this->beginBlock(keyContext, intrinsicCombination, keyBuilder);
if (fNumSlots) {
int numCombinationsSeen = 0;
for (int slotIndex = 0; slotIndex < fNumSlots; ++slotIndex) {
SkSlot* slot = this->getSlot(slotIndex);
numCombinationsSeen += slot->numCombinations();
int numCombosLeft = this->numChildCombinations() / numCombinationsSeen;
int slotCombination;
if (slotIndex+1 < fNumSlots) {
slotCombination = childCombination / (numCombosLeft ? numCombosLeft : 1);
childCombination %= numCombosLeft;
} else {
slotCombination = childCombination;
}
slot->addToKey(keyContext, slotCombination, keyBuilder);
}
}
keyBuilder->endBlock();
}
#ifdef SK_DEBUG
void dump(int indent = 0) const {
SkDebugf("%s", type_to_str(fType));
if (!this->numIntrinsicCombinations()) {
SkDebugf("(%d)", this->numIntrinsicCombinations());
}
SkDebugf(" { %d, %d }", this->numCombinations(), this->numChildCombinations());
if (this->numSlots()) {
SkDebugf("\n");
}
int childIndex = 0;
for (int i = 0; i < fNumSlots; ++i) {
add_indent(indent+4);
SkDebugf("%d: ", childIndex);
fSlots[i].dump(indent+4);
++childIndex;
}
}
#endif
private:
int numIntrinsicCombinations() const;
void beginBlock(const SkKeyContext&, int intrinsicCombination, SkPaintParamsKeyBuilder*) const;
SkDEBUGCODE(static constexpr int kInvalidEpoch = -1;)
const SkShaderType fType;
const int fNumSlots;
SkDEBUGCODE(int fEpoch = kInvalidEpoch;)
SkOption* fNext = nullptr;
SkSlot* fSlots = nullptr; // an array of 'fNumSlots' SkSlots
};
void SkOption::SkSlot::addOption(SkOption* newOption) {
SkASSERT(newOption->fNext == nullptr);
if (fHead == nullptr) {
SkASSERT(fTail == nullptr);
fHead = fTail = newOption;
} else {
SkASSERT(fTail->fNext == nullptr);
fTail->fNext = newOption;
fTail = newOption;
}
}
int SkOption::SkSlot::numCombinations() const {
int numCombinations = 0;
for (SkOption* option = fHead; option; option = option->fNext) {
numCombinations += option->numCombinations();
}
return numCombinations;
}
void SkOption::SkSlot::addToKey(const SkKeyContext& keyContext,
int desiredCombination,
SkPaintParamsKeyBuilder* keyBuilder) {
SkASSERT(desiredCombination < this->numCombinations());
for (SkOption* option = fHead; option; option = option->fNext) {
if (desiredCombination < option->numCombinations()) {
option->addToKey(keyContext, desiredCombination, keyBuilder);
return;
}
desiredCombination -= option->numCombinations();
}
}
#ifdef SK_DEBUG
void SkOption::SkSlot::dump(int indent) const {
SkDebugf("{ %d } ", this->numCombinations());
for (const SkOption* option = fHead; option; option = option->fNext) {
option->dump(indent);
SkDebugf(" | ");
}
SkDebugf("\n");
}
#endif
#define CREATE_ARENA_OBJECT(T, numChildSlots, ...) \
struct ArenaData_##T : public SkOption { \
static const SkShaderType kType = SkShaderType::k##T; \
static const int kNumChildSlots = numChildSlots; \
int numIntrinsicCombinationsDerived() const; \
void beginBlock(const SkKeyContext&, int intrinsicCombination, SkPaintParamsKeyBuilder*) const;\
__VA_ARGS__ \
};
CREATE_ARENA_OBJECT(SolidColor, /* numChildSlots */ 0)
int ArenaData_SolidColor::numIntrinsicCombinationsDerived() const { return 1; }
void ArenaData_SolidColor::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
constexpr SkPMColor4f kUnusedColor = { 1, 0, 0, 1 };
SkASSERT(intrinsicCombination == 0);
SolidColorShaderBlock::BeginBlock(keyContext, builder, /*gatherer=*/nullptr, kUnusedColor);
}
CREATE_ARENA_OBJECT(LinearGradient, /* numChildSlots */ 0,
int fMinNumStops;
int fMaxNumStops;)
int ArenaData_LinearGradient::numIntrinsicCombinationsDerived() const {
return fMaxNumStops - fMinNumStops + 1;
}
void ArenaData_LinearGradient::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
SkASSERT(intrinsicCombination < this->numIntrinsicCombinationsDerived());
GradientShaderBlocks::BeginBlock(keyContext, builder, /*gatherer=*/nullptr,
{ SkShader::kLinear_GradientType,
fMinNumStops + intrinsicCombination });
}
CREATE_ARENA_OBJECT(RadialGradient, /* numChildSlots */ 0,
int fMinNumStops;
int fMaxNumStops;)
int ArenaData_RadialGradient::numIntrinsicCombinationsDerived() const {
return fMaxNumStops - fMinNumStops + 1;
}
void ArenaData_RadialGradient::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
SkASSERT(intrinsicCombination < this->numIntrinsicCombinationsDerived());
GradientShaderBlocks::BeginBlock(keyContext, builder, /*gatherer=*/nullptr,
{ SkShader::kRadial_GradientType,
fMinNumStops + intrinsicCombination });
}
CREATE_ARENA_OBJECT(SweepGradient, /* numChildSlots */ 0,
int fMinNumStops;
int fMaxNumStops;)
int ArenaData_SweepGradient::numIntrinsicCombinationsDerived() const {
return fMaxNumStops - fMinNumStops + 1;
}
void ArenaData_SweepGradient::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
SkASSERT(intrinsicCombination < this->numIntrinsicCombinationsDerived());
GradientShaderBlocks::BeginBlock(keyContext, builder, /*gatherer=*/nullptr,
{ SkShader::kSweep_GradientType,
fMinNumStops + intrinsicCombination });
}
CREATE_ARENA_OBJECT(ConicalGradient, /* numChildSlots */ 0,
int fMinNumStops;
int fMaxNumStops;)
int ArenaData_ConicalGradient::numIntrinsicCombinationsDerived() const {
return fMaxNumStops - fMinNumStops + 1;
}
void ArenaData_ConicalGradient::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
SkASSERT(intrinsicCombination < this->numIntrinsicCombinationsDerived());
GradientShaderBlocks::BeginBlock(keyContext, builder, /*gatherer=*/nullptr,
{ SkShader::kConical_GradientType,
fMinNumStops + intrinsicCombination });
}
CREATE_ARENA_OBJECT(LocalMatrix, /* numChildSlots */ 1)
int ArenaData_LocalMatrix::numIntrinsicCombinationsDerived() const { return 1; }
void ArenaData_LocalMatrix::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
SkASSERT(intrinsicCombination == 0);
LocalMatrixShaderBlock::BeginBlock(keyContext, builder, /*gatherer=*/nullptr,
{ SkMatrix::I() });
}
// Split out due to constructor work
struct ArenaData_Image : public SkOption {
static const SkShaderType kType = SkShaderType::kImage;
static const int kNumChildSlots = 0;
ArenaData_Image(const SkOption& init, SkSpan<SkTileModePair> tileModePairs)
: SkOption(init)
, fTileModeCombos(0) {
for (auto tmPair : tileModePairs) {
int index = tm_pair_to_index(tmPair);
#ifdef SK_DEBUG
SkASSERT(index < 32);
SkTileModePair tmp = index_to_tm_pair(index);
SkASSERT(tmp == tmPair);
#endif
fTileModeCombos |= 0x1 << index;
}
}
int numIntrinsicCombinationsDerived() const;
void beginBlock(const SkKeyContext&, int intrinsicCombination, SkPaintParamsKeyBuilder*) const;
int32_t fTileModeCombos;
};
int ArenaData_Image::numIntrinsicCombinationsDerived() const {
return SkPopCount(fTileModeCombos);
}
void ArenaData_Image::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
SkASSERT(intrinsicCombination < this->numIntrinsicCombinationsDerived());
ImageShaderBlock::BeginBlock(keyContext, builder, /*gatherer=*/nullptr,
// none of the ImageData is used
{ SkSamplingOptions(),
SkTileMode::kClamp, SkTileMode::kClamp,
SkRect::MakeEmpty(), SkMatrix::I() });
}
CREATE_ARENA_OBJECT(BlendShader, /* numChildSlots */ 2)
int ArenaData_BlendShader::numIntrinsicCombinationsDerived() const { return 1; }
void ArenaData_BlendShader::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
SkASSERT(intrinsicCombination == 0);
BlendShaderBlock::BeginBlock(keyContext, builder, /*gatherer=*/nullptr,
{ SkBlendMode::kSrc }); // the blendmode is unused
}
// Here to access the derived ArenaData objects
int SkOption::numIntrinsicCombinations() const {
int numIntrinsicCombinations;
#define CASE(T) \
case SkShaderType::k##T: \
numIntrinsicCombinations = \
static_cast<const ArenaData_##T*>(this)->numIntrinsicCombinationsDerived(); \
break;
switch (this->type()) {
SHADER_TYPES(CASE)
}
#undef CASE
SkASSERT(numIntrinsicCombinations >= 1); // There is always, at least, the existential combo
return numIntrinsicCombinations;
}
// Here to access the derived ArenaData objects
void SkOption::beginBlock(const SkKeyContext& keyContext,
int intrinsicCombination,
SkPaintParamsKeyBuilder* builder) const {
#define CASE(T) \
case SkShaderType::k##T: \
static_cast<const ArenaData_##T*>(this)->beginBlock(keyContext, \
intrinsicCombination, \
builder); \
break;
switch (this->type()) {
SHADER_TYPES(CASE)
}
#undef CASE
}
//--------------------------------------------------------------------------------------------------
SkCombinationOption SkCombinationOption::addChildOption(int childIndex, SkShaderType type) {
if (!this->isValid() || childIndex >= this->numChildSlots()) {
return SkCombinationOption(fBuilder, /*dataInArena=*/nullptr);
}
SkASSERT(fDataInArena->epoch() == fBuilder->fEpoch);
SkOption* child = fBuilder->addOptionInternal(type);
if (child) {
fDataInArena->addOption(childIndex, child);
}
return SkCombinationOption(fBuilder, child);
}
SkCombinationOption SkCombinationOption::addChildOption(int childIndex, SkShaderType type,
int minNumStops, int maxNumStops) {
if (!this->isValid() || childIndex >= this->numChildSlots()) {
return SkCombinationOption(fBuilder, /*dataInArena=*/nullptr);
}
SkASSERT(fDataInArena->epoch() == fBuilder->fEpoch);
SkOption* child = fBuilder->addOptionInternal(type, minNumStops, maxNumStops);
if (child) {
fDataInArena->addOption(childIndex, child);
}
return SkCombinationOption(fBuilder, child);
}
SkCombinationOption SkCombinationOption::addChildOption(
int childIndex, SkShaderType type,
SkSpan<SkTileModePair> tileModes) {
if (!this->isValid() || childIndex >= this->numChildSlots()) {
return SkCombinationOption(fBuilder, /*dataInArena=*/nullptr);
}
SkASSERT(fDataInArena->epoch() == fBuilder->fEpoch);
SkOption* child = fBuilder->addOptionInternal(type, tileModes);
if (child) {
fDataInArena->addOption(childIndex, child);
}
return SkCombinationOption(fBuilder, child);
}
SkShaderType SkCombinationOption::type() const { return fDataInArena->type(); }
int SkCombinationOption::numChildSlots() const { return fDataInArena->numSlots(); }
SkDEBUGCODE(int SkCombinationOption::epoch() const { return fDataInArena->epoch(); })
//--------------------------------------------------------------------------------------------------
SkCombinationBuilder::SkCombinationBuilder(SkShaderCodeDictionary* dict)
: fDictionary(dict) {
fArena = std::make_unique<SkArenaAllocWithReset>(64);
this->reset();
}
SkCombinationBuilder::~SkCombinationBuilder() = default;
template<typename T, typename... Args>
SkOption* SkCombinationBuilder::allocInArena(Args&&... args) {
SkOption* arenaObject = fArena->make<T>(T{{ T::kType, T::kNumChildSlots },
std::forward<Args>(args)... });
if (!arenaObject) {
return nullptr;
}
SkASSERT(arenaObject->type() == T::kType);
SkASSERT(arenaObject->numSlots() == T::kNumChildSlots);
SkDEBUGCODE(arenaObject->setEpoch(fEpoch));
if (T::kNumChildSlots) {
arenaObject->setSlotsArray(fArena->makeArrayDefault<SkOption::SkSlot>(T::kNumChildSlots));
}
return arenaObject;
}
void SkCombinationBuilder::addOption(SkBlendMode bm) {
SkASSERT(fDictionary->isValidID((int) bm));
fBlendModes |= (0x1 << (int) bm);
}
void SkCombinationBuilder::addOption(SkBlendMode rangeStart, SkBlendMode rangeEnd) {
for (int i = (int)rangeStart; i <= (int) rangeEnd; ++i) {
this->addOption((SkBlendMode) i);
}
}
void SkCombinationBuilder::addOption(BlendModeGroup group) {
switch (group) {
case BlendModeGroup::kPorterDuff:
this->addOption(SkBlendMode::kClear, SkBlendMode::kScreen);
break;
case BlendModeGroup::kAdvanced:
this->addOption(SkBlendMode::kOverlay, SkBlendMode::kMultiply);
break;
case BlendModeGroup::kColorAware:
this->addOption(SkBlendMode::kHue, SkBlendMode::kLuminosity);
break;
case BlendModeGroup::kAll:
this->addOption(SkBlendMode::kClear, SkBlendMode::kLastMode);
break;
}
}
void SkCombinationBuilder::addOption(SkBlenderID id) {
SkASSERT(fDictionary->isValidID(id.asUInt()));
fBlenders.add(id);
}
SkOption* SkCombinationBuilder::addOptionInternal(SkShaderType shaderType) {
// TODO: Can we use the X macro trick here to collapse this
switch (shaderType) {
case SkShaderType::kSolidColor:
return this->allocInArena<ArenaData_SolidColor>();
case SkShaderType::kLocalMatrix:
return this->allocInArena<ArenaData_LocalMatrix>();
case SkShaderType::kBlendShader:
return this->allocInArena<ArenaData_BlendShader>();
default:
return nullptr;
}
}
SkOption* SkCombinationBuilder::addOptionInternal(SkShaderType shaderType,
int minNumStops, int maxNumStops) {
// TODO: Can we use the X macro trick here to collapse this
switch (shaderType) {
case SkShaderType::kLinearGradient:
return this->allocInArena<ArenaData_LinearGradient>(minNumStops, maxNumStops);
case SkShaderType::kRadialGradient:
return this->allocInArena<ArenaData_RadialGradient>(minNumStops, maxNumStops);
case SkShaderType::kSweepGradient:
return this->allocInArena<ArenaData_SweepGradient>(minNumStops, maxNumStops);
case SkShaderType::kConicalGradient:
return this->allocInArena<ArenaData_ConicalGradient>(minNumStops, maxNumStops);
default:
return nullptr;
}
}
SkOption* SkCombinationBuilder::addOptionInternal(SkShaderType shaderType,
SkSpan<SkTileModePair> tileModes) {
// TODO: Can we use the X macro trick here to collapse this
switch (shaderType) {
case SkShaderType::kImage:
return this->allocInArena<ArenaData_Image>(tileModes);
default:
return nullptr;
}
}
SkCombinationOption SkCombinationBuilder::addOption(SkShaderType shaderType) {
SkOption* newOption = this->addOptionInternal(shaderType);
if (newOption) {
fShaderOptions.push_back(newOption);
}
return { this, newOption };
}
SkCombinationOption SkCombinationBuilder::addOption(SkShaderType shaderType,
int minNumStops, int maxNumStops) {
SkOption* newOption = this->addOptionInternal(shaderType, minNumStops, maxNumStops);
if (newOption) {
fShaderOptions.push_back(newOption);
}
return { this, newOption };
}
SkCombinationOption SkCombinationBuilder::addOption(SkShaderType shaderType,
SkSpan<SkTileModePair> tileModes) {
SkOption* newOption = this->addOptionInternal(shaderType, tileModes);
if (newOption) {
fShaderOptions.push_back(newOption);
}
return { this, newOption };
}
void SkCombinationBuilder::reset() {
fShaderOptions.reset();
fBlendModes = 0;
fBlenders.reset();
fArena->reset();
SkDEBUGCODE(++fEpoch;)
}
int SkCombinationBuilder::numShaderCombinations() const {
int numShaderCombinations = 0;
for (SkOption* s : fShaderOptions) {
numShaderCombinations += s->numCombinations();
}
// If no shader option is specified the builder will add a solid color shader option
return numShaderCombinations ? numShaderCombinations : 1;
}
int SkCombinationBuilder::numBlendModeCombinations() const {
int numBlendModeCombinations = SkPopCount(fBlendModes) + fBlenders.count();
// If no blend mode options are specified the builder will add kSrcOver as an option
return numBlendModeCombinations ? numBlendModeCombinations : 1;
}
#ifdef SK_DEBUG
void SkCombinationBuilder::dump() const {
for (SkOption* s : fShaderOptions) {
s->dump();
SkDebugf("\n");
}
}
#endif
void SkCombinationBuilder::createKey(const SkKeyContext& keyContext,
int desiredCombination,
SkPaintParamsKeyBuilder* keyBuilder) {
SkDEBUGCODE(keyBuilder->checkReset();)
SkASSERT(desiredCombination < this->numCombinations());
int numBlendModeCombos = this->numBlendModeCombinations();
int desiredShaderCombination = desiredCombination / numBlendModeCombos;
int desiredBlendCombination = desiredCombination % numBlendModeCombos;
for (SkOption* shaderOption : fShaderOptions) {
if (desiredShaderCombination < shaderOption->numCombinations()) {
shaderOption->addToKey(keyContext, desiredShaderCombination, keyBuilder);
break;
}
desiredShaderCombination -= shaderOption->numCombinations();
}
if (desiredBlendCombination < SkPopCount(fBlendModes)) {
int ith_set_bit = SkNthSet(fBlendModes, desiredBlendCombination);
SkASSERT(ith_set_bit < kSkBlendModeCount);
SkBlendMode bm = (SkBlendMode) ith_set_bit;
BlendModeBlock::BeginBlock(keyContext, keyBuilder, /*gatherer=*/nullptr, bm); // bm is used!
keyBuilder->endBlock();
} else {
// TODO: need to handle fBlenders here
}
}
void SkCombinationBuilder::buildCombinations(
SkShaderCodeDictionary* dict,
const std::function<void(SkUniquePaintParamsID)>& func) {
SkKeyContext keyContext(dict);
SkPaintParamsKeyBuilder builder(dict, SkBackend::kGraphite);
// Supply a default kSrcOver if no other blend mode option is provided
if (fBlendModes == 0 && fBlenders.empty()) {
this->addOption(SkBlendMode::kSrcOver);
}
// Supply a default solid color shader if no other shader option is provided
if (fShaderOptions.empty()) {
this->addOption(SkShaderType::kSolidColor);
}
int numCombos = this->numCombinations();
for (int i = 0; i < numCombos; ++i) {
this->createKey(keyContext, i, &builder);
auto entry = dict->findOrCreate(&builder);
func(entry->uniqueID());
}
}