123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- /*
- * Copyright 2015 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
- #include "src/gpu/GrResourceProvider.h"
- #include "include/gpu/GrBackendSemaphore.h"
- #include "include/gpu/GrContext.h"
- #include "include/private/GrResourceKey.h"
- #include "include/private/GrSingleOwner.h"
- #include "src/core/SkConvertPixels.h"
- #include "src/core/SkMathPriv.h"
- #include "src/gpu/GrCaps.h"
- #include "src/gpu/GrContextPriv.h"
- #include "src/gpu/GrGpu.h"
- #include "src/gpu/GrGpuBuffer.h"
- #include "src/gpu/GrPath.h"
- #include "src/gpu/GrPathRendering.h"
- #include "src/gpu/GrProxyProvider.h"
- #include "src/gpu/GrRenderTargetPriv.h"
- #include "src/gpu/GrResourceCache.h"
- #include "src/gpu/GrSemaphore.h"
- #include "src/gpu/GrStencilAttachment.h"
- #include "src/gpu/GrTexturePriv.h"
- #include "src/gpu/SkGr.h"
- const uint32_t GrResourceProvider::kMinScratchTextureSize = 16;
- #define ASSERT_SINGLE_OWNER \
- SkDEBUGCODE(GrSingleOwner::AutoEnforce debug_SingleOwner(fSingleOwner);)
- GrResourceProvider::GrResourceProvider(GrGpu* gpu, GrResourceCache* cache, GrSingleOwner* owner)
- : fCache(cache)
- , fGpu(gpu)
- #ifdef SK_DEBUG
- , fSingleOwner(owner)
- #endif
- {
- fCaps = sk_ref_sp(fGpu->caps());
- }
- // Ensures the row bytes are populated (not 0) and makes a copy to a temporary
- // to make the row bytes tight if necessary. Returns false if the input row bytes are invalid.
- static bool prepare_level(const GrMipLevel& inLevel, size_t bpp, int w, int h, bool rowBytesSupport,
- bool mustInitializeAllLevels, GrMipLevel* outLevel,
- std::unique_ptr<char[]>* data) {
- size_t minRB = w * bpp;
- if (!inLevel.fPixels) {
- if (mustInitializeAllLevels) {
- data->reset(new char[minRB * h]());
- outLevel->fPixels = data->get();
- outLevel->fRowBytes = minRB;
- } else {
- outLevel->fPixels = nullptr;
- outLevel->fRowBytes = 0;
- }
- return true;
- }
- size_t actualRB = inLevel.fRowBytes ? inLevel.fRowBytes : minRB;
- if (actualRB < minRB) {
- return false;
- }
- if (actualRB == minRB || rowBytesSupport) {
- outLevel->fRowBytes = actualRB;
- outLevel->fPixels = inLevel.fPixels;
- } else {
- data->reset(new char[minRB * h]);
- outLevel->fPixels = data->get();
- outLevel->fRowBytes = minRB;
- SkRectMemcpy(data->get(), outLevel->fRowBytes, inLevel.fPixels, inLevel.fRowBytes, minRB,
- h);
- }
- return true;
- }
- sk_sp<GrTexture> GrResourceProvider::createTexture(const GrSurfaceDesc& desc,
- GrRenderable renderable,
- int renderTargetSampleCnt, SkBudgeted budgeted,
- GrProtected isProtected,
- const GrMipLevel texels[], int mipLevelCount) {
- ASSERT_SINGLE_OWNER
- SkASSERT(mipLevelCount > 0);
- if (this->isAbandoned()) {
- return nullptr;
- }
- GrMipMapped mipMapped = mipLevelCount > 1 ? GrMipMapped::kYes : GrMipMapped::kNo;
- if (!fCaps->validateSurfaceDesc(desc, renderable, renderTargetSampleCnt, mipMapped)) {
- return nullptr;
- }
- bool mustInitializeAllLevels = this->caps()->createTextureMustSpecifyAllLevels();
- bool rowBytesSupport = this->caps()->writePixelsRowBytesSupport();
- SkAutoSTMalloc<14, GrMipLevel> tmpTexels;
- SkAutoSTArray<14, std::unique_ptr<char[]>> tmpDatas;
- if (mipLevelCount > 0 && texels) {
- tmpTexels.reset(mipLevelCount);
- tmpDatas.reset(mipLevelCount);
- int w = desc.fWidth;
- int h = desc.fHeight;
- size_t bpp = GrBytesPerPixel(desc.fConfig);
- for (int i = 0; i < mipLevelCount; ++i) {
- if (!prepare_level(texels[i], bpp, w, h, rowBytesSupport, mustInitializeAllLevels,
- &tmpTexels[i], &tmpDatas[i])) {
- return nullptr;
- }
- w = std::max(w / 2, 1);
- h = std::max(h / 2, 1);
- }
- }
- return fGpu->createTexture(desc, renderable, renderTargetSampleCnt, budgeted, isProtected,
- tmpTexels.get(), mipLevelCount);
- }
- sk_sp<GrTexture> GrResourceProvider::getExactScratch(const GrSurfaceDesc& desc,
- GrRenderable renderable,
- int renderTargetSampleCnt,
- SkBudgeted budgeted,
- GrProtected isProtected,
- Flags flags) {
- sk_sp<GrTexture> tex(
- this->refScratchTexture(desc, renderable, renderTargetSampleCnt, isProtected, flags));
- if (tex && SkBudgeted::kNo == budgeted) {
- tex->resourcePriv().makeUnbudgeted();
- }
- return tex;
- }
- sk_sp<GrTexture> GrResourceProvider::createTexture(const GrSurfaceDesc& desc,
- GrRenderable renderable,
- int renderTargetSampleCnt,
- SkBudgeted budgeted,
- SkBackingFit fit,
- GrProtected isProtected,
- const GrMipLevel& mipLevel,
- Flags flags) {
- ASSERT_SINGLE_OWNER
- if (this->isAbandoned()) {
- return nullptr;
- }
- if (!mipLevel.fPixels) {
- return nullptr;
- }
- if (!fCaps->validateSurfaceDesc(desc, renderable, renderTargetSampleCnt, GrMipMapped::kNo)) {
- return nullptr;
- }
- GrContext* context = fGpu->getContext();
- GrProxyProvider* proxyProvider = context->priv().proxyProvider();
- bool mustInitialize = this->caps()->createTextureMustSpecifyAllLevels();
- bool rowBytesSupport = this->caps()->writePixelsRowBytesSupport();
- size_t bpp = GrBytesPerPixel(desc.fConfig);
- std::unique_ptr<char[]> tmpData;
- GrMipLevel tmpLevel;
- if (!prepare_level(mipLevel, bpp, desc.fWidth, desc.fHeight, rowBytesSupport, mustInitialize,
- &tmpLevel, &tmpData)) {
- return nullptr;
- }
- GrColorType colorType = GrPixelConfigToColorType(desc.fConfig);
- sk_sp<GrTexture> tex =
- (SkBackingFit::kApprox == fit)
- ? this->createApproxTexture(desc, renderable, renderTargetSampleCnt,
- isProtected, flags)
- : this->createTexture(desc, renderable, renderTargetSampleCnt, budgeted,
- isProtected, flags);
- if (!tex) {
- return nullptr;
- }
- sk_sp<GrTextureProxy> proxy = proxyProvider->createWrapped(tex, kTopLeft_GrSurfaceOrigin);
- if (!proxy) {
- return nullptr;
- }
- // Here we don't really know the alpha type of the data we want to upload. All we really
- // care about is that it is not converted. So we use the same alpha type for the data
- // and the surface context.
- static constexpr auto kAlphaType = kPremul_SkAlphaType;
- sk_sp<GrSurfaceContext> sContext =
- context->priv().makeWrappedSurfaceContext(std::move(proxy), colorType, kAlphaType);
- if (!sContext) {
- return nullptr;
- }
- GrPixelInfo srcInfo(colorType, kAlphaType, nullptr, desc.fWidth, desc.fHeight);
- SkAssertResult(sContext->writePixels(srcInfo, tmpLevel.fPixels, tmpLevel.fRowBytes, {0, 0}));
- return tex;
- }
- sk_sp<GrTexture> GrResourceProvider::createCompressedTexture(int width, int height,
- SkImage::CompressionType compression,
- SkBudgeted budgeted, SkData* data) {
- ASSERT_SINGLE_OWNER
- if (this->isAbandoned()) {
- return nullptr;
- }
- return fGpu->createCompressedTexture(width, height, compression, budgeted, data->data(),
- data->size());
- }
- sk_sp<GrTexture> GrResourceProvider::createTexture(const GrSurfaceDesc& desc,
- GrRenderable renderable,
- int renderTargetSampleCnt,
- SkBudgeted budgeted,
- GrProtected isProtected,
- Flags flags) {
- ASSERT_SINGLE_OWNER
- if (this->isAbandoned()) {
- return nullptr;
- }
- if (!fCaps->validateSurfaceDesc(desc, renderable, renderTargetSampleCnt, GrMipMapped::kNo)) {
- return nullptr;
- }
- // Compressed textures are read-only so they don't support re-use for scratch.
- if (!GrPixelConfigIsCompressed(desc.fConfig)) {
- sk_sp<GrTexture> tex = this->getExactScratch(desc, renderable, renderTargetSampleCnt,
- budgeted, isProtected, flags);
- if (tex) {
- return tex;
- }
- }
- if (fCaps->createTextureMustSpecifyAllLevels()) {
- size_t rowBytes = GrBytesPerPixel(desc.fConfig) * desc.fWidth;
- size_t size = rowBytes * desc.fHeight;
- std::unique_ptr<char[]> zeros(new char[size]());
- GrMipLevel level;
- level.fRowBytes = rowBytes;
- level.fPixels = zeros.get();
- return fGpu->createTexture(desc, renderable, renderTargetSampleCnt, budgeted, isProtected,
- &level, 1);
- }
- return fGpu->createTexture(desc, renderable, renderTargetSampleCnt, budgeted, isProtected);
- }
- // Map 'value' to a larger multiple of 2. Values <= 'kMagicTol' will pop up to
- // the next power of 2. Those above 'kMagicTol' will only go up half the floor power of 2.
- uint32_t GrResourceProvider::MakeApprox(uint32_t value) {
- static const int kMagicTol = 1024;
- value = SkTMax(kMinScratchTextureSize, value);
- if (SkIsPow2(value)) {
- return value;
- }
- uint32_t ceilPow2 = GrNextPow2(value);
- if (value <= kMagicTol) {
- return ceilPow2;
- }
- uint32_t floorPow2 = ceilPow2 >> 1;
- uint32_t mid = floorPow2 + (floorPow2 >> 1);
- if (value <= mid) {
- return mid;
- }
- return ceilPow2;
- }
- sk_sp<GrTexture> GrResourceProvider::createApproxTexture(const GrSurfaceDesc& desc,
- GrRenderable renderable,
- int renderTargetSampleCnt,
- GrProtected isProtected, Flags flags) {
- ASSERT_SINGLE_OWNER
- SkASSERT(Flags::kNone == flags || Flags::kNoPendingIO == flags);
- if (this->isAbandoned()) {
- return nullptr;
- }
- // Currently we don't recycle compressed textures as scratch.
- if (GrPixelConfigIsCompressed(desc.fConfig)) {
- return nullptr;
- }
- if (!fCaps->validateSurfaceDesc(desc, renderable, renderTargetSampleCnt, GrMipMapped::kNo)) {
- return nullptr;
- }
- if (auto tex = this->refScratchTexture(desc, renderable, renderTargetSampleCnt, isProtected,
- flags)) {
- return tex;
- }
- SkTCopyOnFirstWrite<GrSurfaceDesc> copyDesc(desc);
- // bin by some multiple or power of 2 with a reasonable min
- if (fGpu->caps()->reuseScratchTextures() || renderable == GrRenderable::kYes) {
- GrSurfaceDesc* wdesc = copyDesc.writable();
- wdesc->fWidth = MakeApprox(wdesc->fWidth);
- wdesc->fHeight = MakeApprox(wdesc->fHeight);
- }
- if (auto tex = this->refScratchTexture(*copyDesc, renderable, renderTargetSampleCnt,
- isProtected, flags)) {
- return tex;
- }
- if (this->caps()->createTextureMustSpecifyAllLevels()) {
- size_t rowBytes = GrBytesPerPixel(copyDesc->fConfig) * copyDesc->fWidth;
- size_t size = rowBytes * copyDesc->fHeight;
- std::unique_ptr<char[]> zeros(new char[size]());
- GrMipLevel level;
- level.fRowBytes = rowBytes;
- level.fPixels = zeros.get();
- return fGpu->createTexture(*copyDesc, renderable, renderTargetSampleCnt, SkBudgeted::kYes,
- isProtected, &level, 1);
- }
- return fGpu->createTexture(*copyDesc, renderable, renderTargetSampleCnt, SkBudgeted::kYes,
- isProtected);
- }
- sk_sp<GrTexture> GrResourceProvider::refScratchTexture(const GrSurfaceDesc& desc,
- GrRenderable renderable,
- int renderTargetSampleCnt,
- GrProtected isProtected,
- Flags flags) {
- ASSERT_SINGLE_OWNER
- SkASSERT(!this->isAbandoned());
- SkASSERT(!GrPixelConfigIsCompressed(desc.fConfig));
- SkASSERT(fCaps->validateSurfaceDesc(desc, renderable, renderTargetSampleCnt, GrMipMapped::kNo));
- // We could make initial clears work with scratch textures but it is a rare case so we just opt
- // to fall back to making a new texture.
- if (fGpu->caps()->reuseScratchTextures() || renderable == GrRenderable::kYes) {
- GrScratchKey key;
- GrTexturePriv::ComputeScratchKey(desc, renderable, renderTargetSampleCnt, &key);
- auto scratchFlags = GrResourceCache::ScratchFlags::kNone;
- if (Flags::kNoPendingIO & flags) {
- scratchFlags |= GrResourceCache::ScratchFlags::kRequireNoPendingIO;
- } else if (renderable == GrRenderable::kNo) {
- // If it is not a render target then it will most likely be populated by
- // writePixels() which will trigger a flush if the texture has pending IO.
- scratchFlags |= GrResourceCache::ScratchFlags::kPreferNoPendingIO;
- }
- GrGpuResource* resource = fCache->findAndRefScratchResource(
- key, GrSurface::WorstCaseSize(desc, renderable, renderTargetSampleCnt),
- scratchFlags);
- if (resource) {
- fGpu->stats()->incNumScratchTexturesReused();
- GrSurface* surface = static_cast<GrSurface*>(resource);
- return sk_sp<GrTexture>(surface->asTexture());
- }
- }
- return nullptr;
- }
- sk_sp<GrTexture> GrResourceProvider::wrapBackendTexture(const GrBackendTexture& tex,
- GrColorType colorType,
- GrWrapOwnership ownership,
- GrWrapCacheable cacheable,
- GrIOType ioType) {
- ASSERT_SINGLE_OWNER
- if (this->isAbandoned()) {
- return nullptr;
- }
- return fGpu->wrapBackendTexture(tex, colorType, ownership, cacheable, ioType);
- }
- sk_sp<GrTexture> GrResourceProvider::wrapRenderableBackendTexture(const GrBackendTexture& tex,
- int sampleCnt,
- GrColorType colorType,
- GrWrapOwnership ownership,
- GrWrapCacheable cacheable) {
- ASSERT_SINGLE_OWNER
- if (this->isAbandoned()) {
- return nullptr;
- }
- return fGpu->wrapRenderableBackendTexture(tex, sampleCnt, colorType, ownership, cacheable);
- }
- sk_sp<GrRenderTarget> GrResourceProvider::wrapBackendRenderTarget(
- const GrBackendRenderTarget& backendRT, GrColorType colorType)
- {
- ASSERT_SINGLE_OWNER
- return this->isAbandoned() ? nullptr : fGpu->wrapBackendRenderTarget(backendRT, colorType);
- }
- sk_sp<GrRenderTarget> GrResourceProvider::wrapVulkanSecondaryCBAsRenderTarget(
- const SkImageInfo& imageInfo, const GrVkDrawableInfo& vkInfo) {
- ASSERT_SINGLE_OWNER
- return this->isAbandoned() ? nullptr : fGpu->wrapVulkanSecondaryCBAsRenderTarget(imageInfo,
- vkInfo);
- }
- void GrResourceProvider::assignUniqueKeyToResource(const GrUniqueKey& key,
- GrGpuResource* resource) {
- ASSERT_SINGLE_OWNER
- if (this->isAbandoned() || !resource) {
- return;
- }
- resource->resourcePriv().setUniqueKey(key);
- }
- sk_sp<GrGpuResource> GrResourceProvider::findResourceByUniqueKey(const GrUniqueKey& key) {
- ASSERT_SINGLE_OWNER
- return this->isAbandoned() ? nullptr
- : sk_sp<GrGpuResource>(fCache->findAndRefUniqueResource(key));
- }
- sk_sp<const GrGpuBuffer> GrResourceProvider::findOrMakeStaticBuffer(GrGpuBufferType intendedType,
- size_t size,
- const void* data,
- const GrUniqueKey& key) {
- if (auto buffer = this->findByUniqueKey<GrGpuBuffer>(key)) {
- return std::move(buffer);
- }
- if (auto buffer = this->createBuffer(size, intendedType, kStatic_GrAccessPattern, data)) {
- // We shouldn't bin and/or cache static buffers.
- SkASSERT(buffer->size() == size);
- SkASSERT(!buffer->resourcePriv().getScratchKey().isValid());
- SkASSERT(!buffer->resourcePriv().hasPendingIO_debugOnly());
- buffer->resourcePriv().setUniqueKey(key);
- return sk_sp<const GrGpuBuffer>(buffer);
- }
- return nullptr;
- }
- sk_sp<const GrGpuBuffer> GrResourceProvider::createPatternedIndexBuffer(const uint16_t* pattern,
- int patternSize,
- int reps,
- int vertCount,
- const GrUniqueKey* key) {
- size_t bufferSize = patternSize * reps * sizeof(uint16_t);
- // This is typically used in GrMeshDrawOps, so we assume kNoPendingIO.
- sk_sp<GrGpuBuffer> buffer(
- this->createBuffer(bufferSize, GrGpuBufferType::kIndex, kStatic_GrAccessPattern));
- if (!buffer) {
- return nullptr;
- }
- uint16_t* data = (uint16_t*) buffer->map();
- SkAutoTArray<uint16_t> temp;
- if (!data) {
- temp.reset(reps * patternSize);
- data = temp.get();
- }
- for (int i = 0; i < reps; ++i) {
- int baseIdx = i * patternSize;
- uint16_t baseVert = (uint16_t)(i * vertCount);
- for (int j = 0; j < patternSize; ++j) {
- data[baseIdx+j] = baseVert + pattern[j];
- }
- }
- if (temp.get()) {
- if (!buffer->updateData(data, bufferSize)) {
- return nullptr;
- }
- } else {
- buffer->unmap();
- }
- if (key) {
- SkASSERT(key->isValid());
- this->assignUniqueKeyToResource(*key, buffer.get());
- }
- return std::move(buffer);
- }
- static constexpr int kMaxQuads = 1 << 12; // max possible: (1 << 14) - 1;
- sk_sp<const GrGpuBuffer> GrResourceProvider::createQuadIndexBuffer() {
- GR_STATIC_ASSERT(4 * kMaxQuads <= 65535);
- static const uint16_t kPattern[] = { 0, 1, 2, 2, 1, 3 };
- return this->createPatternedIndexBuffer(kPattern, 6, kMaxQuads, 4, nullptr);
- }
- int GrResourceProvider::QuadCountOfQuadBuffer() { return kMaxQuads; }
- sk_sp<GrPath> GrResourceProvider::createPath(const SkPath& path, const GrStyle& style) {
- if (this->isAbandoned()) {
- return nullptr;
- }
- SkASSERT(this->gpu()->pathRendering());
- return this->gpu()->pathRendering()->createPath(path, style);
- }
- sk_sp<GrGpuBuffer> GrResourceProvider::createBuffer(size_t size, GrGpuBufferType intendedType,
- GrAccessPattern accessPattern,
- const void* data) {
- if (this->isAbandoned()) {
- return nullptr;
- }
- if (kDynamic_GrAccessPattern != accessPattern) {
- return this->gpu()->createBuffer(size, intendedType, accessPattern, data);
- }
- // bin by pow2 with a reasonable min
- static const size_t MIN_SIZE = 1 << 12;
- size_t allocSize = SkTMax(MIN_SIZE, GrNextSizePow2(size));
- GrScratchKey key;
- GrGpuBuffer::ComputeScratchKeyForDynamicVBO(allocSize, intendedType, &key);
- auto buffer =
- sk_sp<GrGpuBuffer>(static_cast<GrGpuBuffer*>(this->cache()->findAndRefScratchResource(
- key, allocSize, GrResourceCache::ScratchFlags::kNone)));
- if (!buffer) {
- buffer = this->gpu()->createBuffer(allocSize, intendedType, kDynamic_GrAccessPattern);
- if (!buffer) {
- return nullptr;
- }
- }
- if (data) {
- buffer->updateData(data, size);
- }
- return buffer;
- }
- bool GrResourceProvider::attachStencilAttachment(GrRenderTarget* rt, int minStencilSampleCount) {
- SkASSERT(rt);
- GrStencilAttachment* stencil = rt->renderTargetPriv().getStencilAttachment();
- if (stencil && stencil->numSamples() >= minStencilSampleCount) {
- return true;
- }
- if (!rt->wasDestroyed() && rt->canAttemptStencilAttachment()) {
- GrUniqueKey sbKey;
- int width = rt->width();
- int height = rt->height();
- #if 0
- if (this->caps()->oversizedStencilSupport()) {
- width = SkNextPow2(width);
- height = SkNextPow2(height);
- }
- #endif
- GrStencilAttachment::ComputeSharedStencilAttachmentKey(
- width, height, minStencilSampleCount, &sbKey);
- auto stencil = this->findByUniqueKey<GrStencilAttachment>(sbKey);
- if (!stencil) {
- // Need to try and create a new stencil
- stencil.reset(this->gpu()->createStencilAttachmentForRenderTarget(
- rt, width, height, minStencilSampleCount));
- if (!stencil) {
- return false;
- }
- this->assignUniqueKeyToResource(sbKey, stencil.get());
- }
- rt->renderTargetPriv().attachStencilAttachment(std::move(stencil));
- }
- if (GrStencilAttachment* stencil = rt->renderTargetPriv().getStencilAttachment()) {
- return stencil->numSamples() >= minStencilSampleCount;
- }
- return false;
- }
- sk_sp<GrRenderTarget> GrResourceProvider::wrapBackendTextureAsRenderTarget(
- const GrBackendTexture& tex, int sampleCnt, GrColorType colorType)
- {
- if (this->isAbandoned()) {
- return nullptr;
- }
- return fGpu->wrapBackendTextureAsRenderTarget(tex, sampleCnt, colorType);
- }
- sk_sp<GrSemaphore> SK_WARN_UNUSED_RESULT GrResourceProvider::makeSemaphore(bool isOwned) {
- return fGpu->makeSemaphore(isOwned);
- }
- sk_sp<GrSemaphore> GrResourceProvider::wrapBackendSemaphore(const GrBackendSemaphore& semaphore,
- SemaphoreWrapType wrapType,
- GrWrapOwnership ownership) {
- ASSERT_SINGLE_OWNER
- return this->isAbandoned() ? nullptr : fGpu->wrapBackendSemaphore(semaphore,
- wrapType,
- ownership);
- }
|