Commit 7960f3b1 authored by Matt Woodrow's avatar Matt Woodrow
Browse files

Bug 1727684 - Remove ContentHost/ImageHost. r=jrmuizel

parent 8577eff6
Loading
Loading
Loading
Loading
+1 −4
Original line number Diff line number Diff line
@@ -156,10 +156,7 @@ enum class EffectTypes : uint8_t {
 */
enum class CompositableType : uint8_t {
  UNKNOWN,
  CONTENT_TILED,   // tiled painted layer
  IMAGE,  // image with single buffering
  CONTENT_SINGLE,  // painted layer interface, single buffering
  CONTENT_DOUBLE,  // painted layer interface, double buffering
  COUNT
};

+2 −52
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@
#include "nsRect.h"                // for IntRect
#include "nsTArray.h"              // for AutoTArray, nsTArray_Impl
#include "mozilla/Poison.h"
#include "mozilla/layers/ImageHost.h"
#include "TreeTraversal.h"  // for ForEachNode

// LayerTreeInvalidation debugging
@@ -613,32 +612,16 @@ struct ColorLayerProperties : public LayerPropertiesBase {
  IntRect mBounds;
};

static ImageHost* GetImageHost(Layer* aLayer) { return nullptr; }

struct ImageLayerProperties : public LayerPropertiesBase {
  explicit ImageLayerProperties(ImageLayer* aImage, bool aIsMask)
      : LayerPropertiesBase(aImage),
        mContainer(aImage->GetContainer()),
        mImageHost(GetImageHost(aImage)),
        mSamplingFilter(aImage->GetSamplingFilter()),
        mScaleToSize(aImage->GetScaleToSize()),
        mScaleMode(aImage->GetScaleMode()),
        mLastProducerID(-1),
        mLastFrameID(-1),
        mIsMask(aIsMask) {
    if (mImageHost) {
      if (aIsMask) {
        // Mask layers never set the 'last' producer/frame
        // id, since they never get composited as their own
        // layer.
        mLastProducerID = mImageHost->GetProducerID();
        mLastFrameID = mImageHost->GetFrameID();
      } else {
        mLastProducerID = mImageHost->GetLastProducerID();
        mLastFrameID = mImageHost->GetLastFrameID();
      }
    }
  }
        mIsMask(aIsMask) {}

  bool ComputeChangeInternal(const char* aPrefix, nsIntRegion& aOutRegion,
                             NotifySubDocInvalidationFunc aCallback) override {
@@ -653,13 +636,10 @@ struct ImageLayerProperties : public LayerPropertiesBase {
    }

    ImageContainer* container = imageLayer->GetContainer();
    ImageHost* host = GetImageHost(imageLayer);
    if (mContainer != container ||
        mSamplingFilter != imageLayer->GetSamplingFilter() ||
        mScaleToSize != imageLayer->GetScaleToSize() ||
        mScaleMode != imageLayer->GetScaleMode() || host != mImageHost ||
        (host && host->GetProducerID() != mLastProducerID) ||
        (host && host->GetFrameID() != mLastFrameID)) {
        mScaleMode != imageLayer->GetScaleMode()) {
      if (mIsMask) {
        // Mask layers have an empty visible region, so we have to
        // use the image size instead.
@@ -667,9 +647,6 @@ struct ImageLayerProperties : public LayerPropertiesBase {
        if (container) {
          size = container->GetCurrentSize();
        }
        if (host) {
          size = host->GetImageSize();
        }
        IntRect rect(0, 0, size.width, size.height);
        LTI_DUMP(rect, "mask");
        aOutRegion = TransformRect(rect, GetTransformForInvalidation(mLayer));
@@ -684,7 +661,6 @@ struct ImageLayerProperties : public LayerPropertiesBase {
  }

  RefPtr<ImageContainer> mContainer;
  RefPtr<ImageHost> mImageHost;
  SamplingFilter mSamplingFilter;
  gfx::IntSize mScaleToSize;
  ScaleMode mScaleMode;
@@ -693,30 +669,6 @@ struct ImageLayerProperties : public LayerPropertiesBase {
  bool mIsMask;
};

struct CanvasLayerProperties : public LayerPropertiesBase {
  explicit CanvasLayerProperties(CanvasLayer* aCanvas)
      : LayerPropertiesBase(aCanvas), mImageHost(GetImageHost(aCanvas)) {
    mFrameID = mImageHost ? mImageHost->GetFrameID() : -1;
  }

  bool ComputeChangeInternal(const char* aPrefix, nsIntRegion& aOutRegion,
                             NotifySubDocInvalidationFunc aCallback) override {
    CanvasLayer* canvasLayer = static_cast<CanvasLayer*>(mLayer.get());

    ImageHost* host = GetImageHost(canvasLayer);
    if (host && host->GetFrameID() != mFrameID) {
      LTI_DUMP(NewTransformedBoundsForLeaf(), "frameId");
      aOutRegion = NewTransformedBoundsForLeaf();
      return true;
    }

    return true;
  }

  RefPtr<ImageHost> mImageHost;
  int32_t mFrameID;
};

UniquePtr<LayerPropertiesBase> CloneLayerTreePropertiesInternal(
    Layer* aRoot, bool aIsMask /* = false */) {
  if (!aRoot) {
@@ -737,8 +689,6 @@ UniquePtr<LayerPropertiesBase> CloneLayerTreePropertiesInternal(
      return MakeUnique<ImageLayerProperties>(static_cast<ImageLayer*>(aRoot),
                                              aIsMask);
    case Layer::TYPE_CANVAS:
      return MakeUnique<CanvasLayerProperties>(
          static_cast<CanvasLayer*>(aRoot));
    case Layer::TYPE_DISPLAYITEM:
    case Layer::TYPE_READBACK:
    case Layer::TYPE_SHADOW:
+5 −22
Original line number Diff line number Diff line
@@ -7,10 +7,8 @@
#include "CompositableHost.h"
#include <map>        // for _Rb_tree_iterator, map, etc
#include <utility>    // for pair
#include "ContentHost.h"  // for ContentHostDoubleBuffered, etc
#include "Effects.h"  // for EffectMask, Effect, etc
#include "gfxUtils.h"
#include "ImageHost.h"  // for ImageHostBuffered, etc
#include "Layers.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/layers/LayersSurfaces.h"  // for SurfaceDescriptor
@@ -106,26 +104,11 @@ void CompositableHost::RemoveMaskEffect() {

/* static */
already_AddRefed<CompositableHost> CompositableHost::Create(
    const TextureInfo& aTextureInfo, bool aUseWebRender) {
    const TextureInfo& aTextureInfo) {
  RefPtr<CompositableHost> result;
  switch (aTextureInfo.mCompositableType) {
    case CompositableType::IMAGE:
      if (aUseWebRender) {
      result = new WebRenderImageHost(aTextureInfo);
      } else {
        result = new ImageHost(aTextureInfo);
      }
      break;
    case CompositableType::CONTENT_SINGLE:
      if (aUseWebRender) {
        result = new WebRenderImageHost(aTextureInfo);
      } else {
        result = new ContentHostSingleBuffered(aTextureInfo);
      }
      break;
    case CompositableType::CONTENT_DOUBLE:
      MOZ_ASSERT(!aUseWebRender);
      result = new ContentHostDoubleBuffered(aTextureInfo);
      break;
    default:
      NS_ERROR("Unknown CompositableType");
+1 −1
Original line number Diff line number Diff line
@@ -88,7 +88,7 @@ class CompositableHost {
  explicit CompositableHost(const TextureInfo& aTextureInfo);

  static already_AddRefed<CompositableHost> Create(
      const TextureInfo& aTextureInfo, bool aUseWebRender);
      const TextureInfo& aTextureInfo);

  virtual CompositableType GetType() = 0;

+0 −460
Original line number Diff line number Diff line
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/layers/ContentHost.h"
#include "gfx2DGlue.h"                      // for ContentForFormat
#include "mozilla/gfx/Point.h"              // for IntSize
#include "mozilla/Assertions.h"             // for MOZ_ASSERT, etc
#include "mozilla/gfx/BaseRect.h"           // for BaseRect
#include "mozilla/layers/Compositor.h"      // for Compositor
#include "mozilla/layers/Effects.h"         // for TexturedEffect, Effect, etc
#include "mozilla/layers/LayersMessages.h"  // for ThebesBufferData
#include "nsAString.h"
#include "nsPrintfCString.h"                // for nsPrintfCString
#include "nsString.h"                       // for nsAutoCString
#include "mozilla/layers/TextureHostOGL.h"  // for TextureHostOGL

namespace mozilla {
using namespace gfx;

namespace layers {

ContentHostBase::ContentHostBase(const TextureInfo& aTextureInfo)
    : ContentHost(aTextureInfo), mInitialised(false) {}

ContentHostBase::~ContentHostBase() = default;

void ContentHostTexture::Composite(
    Compositor* aCompositor, LayerComposite* aLayer, EffectChain& aEffectChain,
    float aOpacity, const gfx::Matrix4x4& aTransform,
    const SamplingFilter aSamplingFilter, const IntRect& aClipRect,
    const nsIntRegion* aVisibleRegion, const Maybe<gfx::Polygon>& aGeometry) {
  NS_ASSERTION(aVisibleRegion, "Requires a visible region");

  AutoLockCompositableHost lock(this);
  if (lock.Failed()) {
    return;
  }

  if (!mTextureHost->BindTextureSource(mTextureSource)) {
    return;
  }
  MOZ_ASSERT(mTextureSource.get());

  if (!mTextureHostOnWhite) {
    mTextureSourceOnWhite = nullptr;
  }
  if (mTextureHostOnWhite &&
      !mTextureHostOnWhite->BindTextureSource(mTextureSourceOnWhite)) {
    return;
  }

  RefPtr<TexturedEffect> effect = CreateTexturedEffect(
      mTextureSource.get(), mTextureSourceOnWhite.get(), aSamplingFilter, true);
  if (!effect) {
    return;
  }

  aEffectChain.mPrimaryEffect = effect;

  nsIntRegion tmpRegion;
  const nsIntRegion* renderRegion;
#ifndef MOZ_IGNORE_PAINT_WILL_RESAMPLE
  if (PaintWillResample()) {
    // If we're resampling, then the texture image will contain exactly the
    // entire visible region's bounds, and we should draw it all in one quad
    // to avoid unexpected aliasing.
    tmpRegion = aVisibleRegion->GetBounds();
    renderRegion = &tmpRegion;
  } else {
    renderRegion = aVisibleRegion;
  }
#else
  renderRegion = aVisibleRegion;
#endif

  nsIntRegion region(*renderRegion);
  nsIntPoint origin = GetOriginOffset();
  // translate into TexImage space, buffer origin might not be at texture (0,0)
  region.MoveBy(-origin);

  // Figure out the intersecting draw region
  gfx::IntSize texSize = mTextureSource->GetSize();
  IntRect textureRect = IntRect(0, 0, texSize.width, texSize.height);
  textureRect.MoveBy(region.GetBounds().TopLeft());
  nsIntRegion subregion;
  subregion.And(region, textureRect);
  if (subregion.IsEmpty()) {
    // Region is empty, nothing to draw
    return;
  }

  nsIntRegion screenRects;
  nsIntRegion regionRects;

  // Collect texture/screen coordinates for drawing
  for (auto iter = subregion.RectIter(); !iter.Done(); iter.Next()) {
    IntRect regionRect = iter.Get();
    IntRect screenRect = iter.Get();
    screenRect.MoveBy(origin);

    screenRects.Or(screenRects, screenRect);
    regionRects.Or(regionRects, regionRect);
  }

  BigImageIterator* bigImgIter = mTextureSource->AsBigImageIterator();
  BigImageIterator* iterOnWhite = nullptr;
  if (bigImgIter) {
    bigImgIter->BeginBigImageIteration();
  }

  if (mTextureSourceOnWhite) {
    iterOnWhite = mTextureSourceOnWhite->AsBigImageIterator();
    MOZ_ASSERT(!bigImgIter ||
                   bigImgIter->GetTileCount() == iterOnWhite->GetTileCount(),
               "Tile count mismatch on component alpha texture");
    if (iterOnWhite) {
      iterOnWhite->BeginBigImageIteration();
    }
  }

  bool usingTiles = (bigImgIter && bigImgIter->GetTileCount() > 1);
  do {
    if (iterOnWhite && bigImgIter) {
      MOZ_ASSERT(iterOnWhite->GetTileRect() == bigImgIter->GetTileRect(),
                 "component alpha textures should be the same size.");
    }

    IntRect texRect = bigImgIter ? bigImgIter->GetTileRect()
                                 : IntRect(0, 0, texSize.width, texSize.height);

    // Draw texture. If we're using tiles, we do repeating manually, as texture
    // repeat would cause each individual tile to repeat instead of the
    // compound texture as a whole. This involves drawing at most 4 sections,
    // 2 for each axis that has texture repeat.
    for (int y = 0; y < (usingTiles ? 2 : 1); y++) {
      for (int x = 0; x < (usingTiles ? 2 : 1); x++) {
        IntRect currentTileRect(texRect);
        currentTileRect.MoveBy(x * texSize.width, y * texSize.height);

        for (auto screenIter = screenRects.RectIter(),
                  regionIter = regionRects.RectIter();
             !screenIter.Done() && !regionIter.Done();
             screenIter.Next(), regionIter.Next()) {
          const IntRect& screenRect = screenIter.Get();
          const IntRect& regionRect = regionIter.Get();
          IntRect tileScreenRect(screenRect);
          IntRect tileRegionRect(regionRect);

          // When we're using tiles, find the intersection between the tile
          // rect and this region rect. Tiling is then handled by the
          // outer for-loops and modifying the tile rect.
          if (usingTiles) {
            tileScreenRect.MoveBy(-origin);
            tileScreenRect = tileScreenRect.Intersect(currentTileRect);
            tileScreenRect.MoveBy(origin);

            if (tileScreenRect.IsEmpty()) continue;

            tileRegionRect = regionRect.Intersect(currentTileRect);
            tileRegionRect.MoveBy(-currentTileRect.TopLeft());
          }
          gfx::Rect rect(tileScreenRect.X(), tileScreenRect.Y(),
                         tileScreenRect.Width(), tileScreenRect.Height());

          effect->mTextureCoords =
              Rect(Float(tileRegionRect.X()) / texRect.Width(),
                   Float(tileRegionRect.Y()) / texRect.Height(),
                   Float(tileRegionRect.Width()) / texRect.Width(),
                   Float(tileRegionRect.Height()) / texRect.Height());

          aCompositor->DrawGeometry(rect, aClipRect, aEffectChain, aOpacity,
                                    aTransform, aGeometry);

          if (usingTiles) {
            DiagnosticFlags diagnostics =
                DiagnosticFlags::CONTENT | DiagnosticFlags::BIGIMAGE;
            if (iterOnWhite) {
              diagnostics |= DiagnosticFlags::COMPONENT_ALPHA;
            }
            aCompositor->DrawDiagnostics(diagnostics, rect, aClipRect,
                                         aTransform, mFlashCounter);
          }
        }
      }
    }

    if (iterOnWhite) {
      iterOnWhite->NextTile();
    }
  } while (usingTiles && bigImgIter->NextTile());

  if (bigImgIter) {
    bigImgIter->EndBigImageIteration();
  }
  if (iterOnWhite) {
    iterOnWhite->EndBigImageIteration();
  }

  DiagnosticFlags diagnostics = DiagnosticFlags::CONTENT;
  if (iterOnWhite) {
    diagnostics |= DiagnosticFlags::COMPONENT_ALPHA;
  }
  aCompositor->DrawDiagnostics(diagnostics, nsIntRegion(mBufferRect), aClipRect,
                               aTransform, mFlashCounter);
}

RefPtr<TextureSource> ContentHostTexture::AcquireTextureSource() {
  if (!mTextureHost || !mTextureHost->AcquireTextureSource(mTextureSource)) {
    return nullptr;
  }
  return mTextureSource.get();
}

RefPtr<TextureSource> ContentHostTexture::AcquireTextureSourceOnWhite() {
  if (!mTextureHostOnWhite ||
      !mTextureHostOnWhite->AcquireTextureSource(mTextureSourceOnWhite)) {
    return nullptr;
  }
  return mTextureSourceOnWhite.get();
}

void ContentHostTexture::UseTextureHost(
    const nsTArray<TimedTexture>& aTextures) {
  ContentHostBase::UseTextureHost(aTextures);
  MOZ_ASSERT(aTextures.Length() == 1);
  const TimedTexture& t = aTextures[0];
  MOZ_ASSERT(t.mPictureRect.IsEqualInterior(
                 nsIntRect(nsIntPoint(0, 0), nsIntSize(t.mTexture->GetSize()))),
             "Only default picture rect supported");

  if (t.mTexture != mTextureHost) {
    mReceivedNewHost = true;
  }

  mTextureHost = t.mTexture;
  mTextureHostOnWhite = nullptr;
  mTextureSourceOnWhite = nullptr;
  if (mTextureHost) {
    mTextureHost->PrepareTextureSource(mTextureSource);
  }
}

void ContentHostTexture::UseComponentAlphaTextures(
    TextureHost* aTextureOnBlack, TextureHost* aTextureOnWhite) {
  ContentHostBase::UseComponentAlphaTextures(aTextureOnBlack, aTextureOnWhite);
  mTextureHost = aTextureOnBlack;
  mTextureHostOnWhite = aTextureOnWhite;
  if (mTextureHost) {
    mTextureHost->PrepareTextureSource(mTextureSource);
  }
  if (mTextureHostOnWhite) {
    mTextureHostOnWhite->PrepareTextureSource(mTextureSourceOnWhite);
  }
}

void ContentHostTexture::SetTextureSourceProvider(
    TextureSourceProvider* aProvider) {
  ContentHostBase::SetTextureSourceProvider(aProvider);
  if (mTextureHost) {
    mTextureHost->SetTextureSourceProvider(aProvider);
  }
  if (mTextureHostOnWhite) {
    mTextureHostOnWhite->SetTextureSourceProvider(aProvider);
  }
}

void ContentHostTexture::Dump(std::stringstream& aStream, const char* aPrefix,
                              bool aDumpHtml) {
#ifdef MOZ_DUMP_PAINTING
  if (aDumpHtml) {
    aStream << "<ul>";
  }
  if (mTextureHost) {
    aStream << aPrefix;
    if (aDumpHtml) {
      aStream << "<li> <a href=";
    } else {
      aStream << "Front buffer: ";
    }
    DumpTextureHost(aStream, mTextureHost);
    if (aDumpHtml) {
      aStream << "> Front buffer </a></li> ";
    } else {
      aStream << "\n";
    }
  }
  if (mTextureHostOnWhite) {
    aStream << aPrefix;
    if (aDumpHtml) {
      aStream << "<li> <a href=";
    } else {
      aStream << "Front buffer on white: ";
    }
    DumpTextureHost(aStream, mTextureHostOnWhite);
    if (aDumpHtml) {
      aStream << "> Front buffer on white </a> </li> ";
    } else {
      aStream << "\n";
    }
  }
  if (aDumpHtml) {
    aStream << "</ul>";
  }
#endif
}

static inline void AddWrappedRegion(const nsIntRegion& aInput,
                                    nsIntRegion& aOutput, const IntSize& aSize,
                                    const nsIntPoint& aShift) {
  nsIntRegion tempRegion;
  tempRegion.And(IntRect(aShift, aSize), aInput);
  tempRegion.MoveBy(-aShift);
  aOutput.Or(aOutput, tempRegion);
}

bool ContentHostSingleBuffered::UpdateThebes(
    const ThebesBufferData& aData, const nsIntRegion& aUpdated,
    const nsIntRegion& aOldValidRegionBack) {
  if (!mTextureHost) {
    mInitialised = false;
    return true;  // FIXME should we return false? Returning true for now
  }               // to preserve existing behavior of NOT causing IPC errors.

  // updated is in screen coordinates. Convert it to buffer coordinates.
  nsIntRegion destRegion(aUpdated);

  if (mReceivedNewHost) {
    destRegion.Or(destRegion, aOldValidRegionBack);
    mReceivedNewHost = false;
  }
  destRegion.MoveBy(-aData.rect().TopLeft());

  if (!aData.rect().Contains(aUpdated.GetBounds()) ||
      aData.rotation().x > aData.rect().Width() ||
      aData.rotation().y > aData.rect().Height()) {
    NS_ERROR("Invalid update data");
    return false;
  }

  // destRegion is now in logical coordinates relative to the buffer, but we
  // need to account for rotation. We do that by moving the region to the
  // rotation offset and then wrapping any pixels that extend off the
  // bottom/right edges.

  // Shift to the rotation point
  destRegion.MoveBy(aData.rotation());

  IntSize bufferSize = aData.rect().Size();

  // Select only the pixels that are still within the buffer.
  nsIntRegion finalRegion;
  finalRegion.And(IntRect(IntPoint(), bufferSize), destRegion);

  // For each of the overlap areas (right, bottom-right, bottom), select those
  // pixels and wrap them around to the opposite edge of the buffer rect.
  AddWrappedRegion(destRegion, finalRegion, bufferSize,
                   nsIntPoint(aData.rect().Width(), 0));
  AddWrappedRegion(destRegion, finalRegion, bufferSize,
                   nsIntPoint(aData.rect().Width(), aData.rect().Height()));
  AddWrappedRegion(destRegion, finalRegion, bufferSize,
                   nsIntPoint(0, aData.rect().Height()));

  MOZ_ASSERT(IntRect(0, 0, aData.rect().Width(), aData.rect().Height())
                 .Contains(finalRegion.GetBounds()));

  mTextureHost->Updated(&finalRegion);
  if (mTextureHostOnWhite) {
    mTextureHostOnWhite->Updated(&finalRegion);
  }
  mInitialised = true;

  mBufferRect = aData.rect();
  mBufferRotation = aData.rotation();

  return true;
}

bool ContentHostDoubleBuffered::UpdateThebes(
    const ThebesBufferData& aData, const nsIntRegion& aUpdated,
    const nsIntRegion& aOldValidRegionBack) {
  if (!mTextureHost) {
    mInitialised = false;
    return true;
  }

  // We don't need to calculate an update region because we assume that if we
  // are using double buffering then we have render-to-texture and thus no
  // upload to do.
  mTextureHost->Updated();
  if (mTextureHostOnWhite) {
    mTextureHostOnWhite->Updated();
  }
  mInitialised = true;

  mBufferRect = aData.rect();
  mBufferRotation = aData.rotation();

  // Save the current valid region of our front buffer, because if
  // we're double buffering, it's going to be the valid region for the
  // next back buffer sent back to the renderer.
  //
  // NB: we rely here on the fact that mValidRegion is initialized to
  // empty, and that the first time Swap() is called we don't have a
  // valid front buffer that we're going to return to content.
  mValidRegionForNextBackBuffer = aOldValidRegionBack;

  return true;
}

void ContentHostTexture::PrintInfo(std::stringstream& aStream,
                                   const char* aPrefix) {
  aStream << aPrefix;
  aStream << nsPrintfCString("ContentHost (0x%p)", this).get()
          << " [buffer-rect=" << mBufferRect << "]"
          << " [buffer-rotation=" << mBufferRotation << "]";
  if (PaintWillResample()) {
    aStream << " [paint-will-resample]";
  }

  if (mTextureHost) {
    nsAutoCString pfx(aPrefix);
    pfx += "  ";

    aStream << "\n";
    mTextureHost->PrintInfo(aStream, pfx.get());
  }
}

already_AddRefed<TexturedEffect> ContentHostTexture::GenEffect(
    const gfx::SamplingFilter aSamplingFilter) {
  if (!mTextureHost) {
    return nullptr;
  }
  if (!mTextureHost->BindTextureSource(mTextureSource)) {
    return nullptr;
  }
  if (!mTextureHostOnWhite) {
    mTextureSourceOnWhite = nullptr;
  }
  if (mTextureHostOnWhite &&
      !mTextureHostOnWhite->BindTextureSource(mTextureSourceOnWhite)) {
    return nullptr;
  }
  return CreateTexturedEffect(mTextureSource.get(), mTextureSourceOnWhite.get(),
                              aSamplingFilter, true);
}

already_AddRefed<gfx::DataSourceSurface> ContentHostTexture::GetAsSurface() {
  if (!mTextureHost) {
    return nullptr;
  }

  return mTextureHost->GetAsSurface();
}

}  // namespace layers
}  // namespace mozilla
Loading