зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1083101 - Implement gfx::DrawingTask. r=jrmuizel
This commit is contained in:
Родитель
6c5cc2c997
Коммит
3ef74569e7
|
@ -39,7 +39,7 @@ class DrawingCommand
|
||||||
public:
|
public:
|
||||||
virtual ~DrawingCommand() {}
|
virtual ~DrawingCommand() {}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix& aTransform) = 0;
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix* aTransform = nullptr) = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
explicit DrawingCommand(CommandType aType)
|
explicit DrawingCommand(CommandType aType)
|
||||||
|
@ -130,7 +130,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->DrawSurface(mSurface, mDest, mSource, mSurfOptions, mOptions);
|
aDT->DrawSurface(mSurface, mDest, mSource, mSurfOptions, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->DrawFilter(mFilter, mSourceRect, mDestPoint, mOptions);
|
aDT->DrawFilter(mFilter, mSourceRect, mDestPoint, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -175,7 +175,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->ClearRect(mRect);
|
aDT->ClearRect(mRect);
|
||||||
}
|
}
|
||||||
|
@ -197,11 +197,13 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix& aTransform)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix* aTransform)
|
||||||
{
|
{
|
||||||
MOZ_ASSERT(!aTransform.HasNonIntegerTranslation());
|
MOZ_ASSERT(!aTransform || !aTransform->HasNonIntegerTranslation());
|
||||||
Point dest(Float(mDestination.x), Float(mDestination.y));
|
Point dest(Float(mDestination.x), Float(mDestination.y));
|
||||||
dest = aTransform * dest;
|
if (aTransform) {
|
||||||
|
dest = (*aTransform) * dest;
|
||||||
|
}
|
||||||
aDT->CopySurface(mSurface, mSourceRect, IntPoint(uint32_t(dest.x), uint32_t(dest.y)));
|
aDT->CopySurface(mSurface, mSourceRect, IntPoint(uint32_t(dest.x), uint32_t(dest.y)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,7 +226,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->FillRect(mRect, mPattern, mOptions);
|
aDT->FillRect(mRect, mPattern, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -250,7 +252,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->StrokeRect(mRect, mPattern, mStrokeOptions, mOptions);
|
aDT->StrokeRect(mRect, mPattern, mStrokeOptions, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -279,7 +281,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->StrokeLine(mStart, mEnd, mPattern, mStrokeOptions, mOptions);
|
aDT->StrokeLine(mStart, mEnd, mPattern, mStrokeOptions, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -305,7 +307,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->Fill(mPath, mPattern, mOptions);
|
aDT->Fill(mPath, mPattern, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -331,7 +333,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->Stroke(mPath, mPattern, mStrokeOptions, mOptions);
|
aDT->Stroke(mPath, mPattern, mStrokeOptions, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -361,7 +363,7 @@ public:
|
||||||
memcpy(&mGlyphs.front(), aBuffer.mGlyphs, sizeof(Glyph) * aBuffer.mNumGlyphs);
|
memcpy(&mGlyphs.front(), aBuffer.mGlyphs, sizeof(Glyph) * aBuffer.mNumGlyphs);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
GlyphBuffer buf;
|
GlyphBuffer buf;
|
||||||
buf.mNumGlyphs = mGlyphs.size();
|
buf.mNumGlyphs = mGlyphs.size();
|
||||||
|
@ -390,7 +392,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->Mask(mSource, mMask, mOptions);
|
aDT->Mask(mSource, mMask, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -416,7 +418,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->MaskSurface(mSource, mMask, mOffset, mOptions);
|
aDT->MaskSurface(mSource, mMask, mOffset, mOptions);
|
||||||
}
|
}
|
||||||
|
@ -437,7 +439,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->PushClip(mPath);
|
aDT->PushClip(mPath);
|
||||||
}
|
}
|
||||||
|
@ -455,7 +457,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->PushClipRect(mRect);
|
aDT->PushClipRect(mRect);
|
||||||
}
|
}
|
||||||
|
@ -472,7 +474,7 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix&)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix*)
|
||||||
{
|
{
|
||||||
aDT->PopClip();
|
aDT->PopClip();
|
||||||
}
|
}
|
||||||
|
@ -487,11 +489,13 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix& aMatrix)
|
virtual void ExecuteOnDT(DrawTarget* aDT, const Matrix* aMatrix)
|
||||||
{
|
{
|
||||||
Matrix transform = mTransform;
|
if (aMatrix) {
|
||||||
transform *= aMatrix;
|
aDT->SetTransform(mTransform * (*aMatrix));
|
||||||
aDT->SetTransform(transform);
|
} else {
|
||||||
|
aDT->SetTransform(mTransform);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -188,7 +188,7 @@ DrawTargetCaptureImpl::ReplayToDrawTarget(DrawTarget* aDT, const Matrix& aTransf
|
||||||
uint8_t* current = start;
|
uint8_t* current = start;
|
||||||
|
|
||||||
while (current < start + mDrawCommandStorage.size()) {
|
while (current < start + mDrawCommandStorage.size()) {
|
||||||
reinterpret_cast<DrawingCommand*>(current + sizeof(uint32_t))->ExecuteOnDT(aDT, aTransform);
|
reinterpret_cast<DrawingCommand*>(current + sizeof(uint32_t))->ExecuteOnDT(aDT, &aTransform);
|
||||||
current += *(uint32_t*)current;
|
current += *(uint32_t*)current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
||||||
|
* 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 "DrawingTask.h"
|
||||||
|
#include "TaskScheduler.h"
|
||||||
|
#include "mozilla/gfx/2D.h"
|
||||||
|
|
||||||
|
namespace mozilla {
|
||||||
|
namespace gfx {
|
||||||
|
|
||||||
|
DrawingTaskBuilder::DrawingTaskBuilder()
|
||||||
|
: mTask(nullptr)
|
||||||
|
{}
|
||||||
|
|
||||||
|
DrawingTaskBuilder::~DrawingTaskBuilder()
|
||||||
|
{
|
||||||
|
if (mTask) {
|
||||||
|
delete mTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DrawingTask::Clear()
|
||||||
|
{
|
||||||
|
mCommandBuffer = nullptr;
|
||||||
|
mCursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DrawingTaskBuilder::BeginDrawingTask(MultiThreadedTaskQueue* aTaskQueue,
|
||||||
|
DrawTarget* aTarget, IntPoint aOffset,
|
||||||
|
SyncObject* aStart)
|
||||||
|
{
|
||||||
|
MOZ_ASSERT(!mTask);
|
||||||
|
MOZ_ASSERT(aTaskQueue);
|
||||||
|
mTask = new DrawingTask(aTaskQueue, aTarget, aOffset, aStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawingTask*
|
||||||
|
DrawingTaskBuilder::EndDrawingTask(CommandBuffer* aCmdBuffer, SyncObject* aCompletion)
|
||||||
|
{
|
||||||
|
MOZ_ASSERT(mTask);
|
||||||
|
mTask->mCompletionSync = aCompletion;
|
||||||
|
mTask->mCommandBuffer = aCmdBuffer;
|
||||||
|
DrawingTask* task = mTask;
|
||||||
|
mTask = nullptr;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawingTask::DrawingTask(MultiThreadedTaskQueue* aTaskQueue,
|
||||||
|
DrawTarget* aTarget, IntPoint aOffset,
|
||||||
|
SyncObject* aStart)
|
||||||
|
: Task(aTaskQueue, aStart, nullptr)
|
||||||
|
, mCommandBuffer(nullptr)
|
||||||
|
, mCursor(0)
|
||||||
|
, mDrawTarget(aTarget)
|
||||||
|
, mOffset(aOffset)
|
||||||
|
{
|
||||||
|
mCommandOffsets.reserve(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskStatus
|
||||||
|
DrawingTask::Run()
|
||||||
|
{
|
||||||
|
while (mCursor < mCommandOffsets.size()) {
|
||||||
|
|
||||||
|
DrawingCommand* cmd = mCommandBuffer->GetDrawingCommand(mCommandOffsets[mCursor]);
|
||||||
|
|
||||||
|
if (!cmd) {
|
||||||
|
return TaskStatus::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd->ExecuteOnDT(mDrawTarget);
|
||||||
|
|
||||||
|
++mCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus::Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawingTask::~DrawingTask()
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawingCommand*
|
||||||
|
CommandBuffer::GetDrawingCommand(ptrdiff_t aId)
|
||||||
|
{
|
||||||
|
return static_cast<DrawingCommand*>(mStorage.GetStorage(aId));
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandBuffer::~CommandBuffer()
|
||||||
|
{
|
||||||
|
mStorage.ForEach([](void* item){
|
||||||
|
static_cast<DrawingCommand*>(item)->~DrawingCommand();
|
||||||
|
});
|
||||||
|
mStorage.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
CommandBufferBuilder::BeginCommandBuffer(size_t aBufferSize)
|
||||||
|
{
|
||||||
|
MOZ_ASSERT(!mCommands);
|
||||||
|
mCommands = new CommandBuffer(aBufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
already_AddRefed<CommandBuffer>
|
||||||
|
CommandBufferBuilder::EndCommandBuffer()
|
||||||
|
{
|
||||||
|
return mCommands.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace
|
|
@ -0,0 +1,153 @@
|
||||||
|
/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
#ifndef MOZILLA_GFX_COMMANDBUFFER_H_
|
||||||
|
#define MOZILLA_GFX_COMMANDBUFFER_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "mozilla/RefPtr.h"
|
||||||
|
#include "mozilla/Assertions.h"
|
||||||
|
#include "mozilla/gfx/Matrix.h"
|
||||||
|
#include "mozilla/gfx/TaskScheduler.h"
|
||||||
|
#include "mozilla/gfx/IterableArena.h"
|
||||||
|
#include "DrawCommand.h"
|
||||||
|
|
||||||
|
namespace mozilla {
|
||||||
|
namespace gfx {
|
||||||
|
|
||||||
|
class DrawingCommand;
|
||||||
|
class PrintCommand;
|
||||||
|
class SignalCommand;
|
||||||
|
class DrawingTask;
|
||||||
|
class WaitCommand;
|
||||||
|
|
||||||
|
class SyncObject;
|
||||||
|
class MultiThreadedTaskQueue;
|
||||||
|
|
||||||
|
class DrawTarget;
|
||||||
|
|
||||||
|
class DrawingTaskBuilder;
|
||||||
|
class CommandBufferBuilder;
|
||||||
|
|
||||||
|
/// Contains a sequence of immutable drawing commands that are typically used by
|
||||||
|
/// several DrawingTasks.
|
||||||
|
///
|
||||||
|
/// CommandBuffer objects are built using CommandBufferBuilder.
|
||||||
|
class CommandBuffer : public external::AtomicRefCounted<CommandBuffer>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MOZ_DECLARE_REFCOUNTED_TYPENAME(CommandBuffer)
|
||||||
|
|
||||||
|
~CommandBuffer();
|
||||||
|
|
||||||
|
DrawingCommand* GetDrawingCommand(ptrdiff_t aId);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
CommandBuffer(size_t aSize = 256)
|
||||||
|
: mStorage(IterableArena::GROWABLE, aSize)
|
||||||
|
{}
|
||||||
|
|
||||||
|
IterableArena mStorage;
|
||||||
|
friend class CommandBufferBuilder;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generates CommandBuffer objects.
|
||||||
|
///
|
||||||
|
/// The builder is a separate object to ensure that commands are not added to a
|
||||||
|
/// submitted CommandBuffer.
|
||||||
|
class CommandBufferBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void BeginCommandBuffer(size_t aBufferSize = 256);
|
||||||
|
|
||||||
|
already_AddRefed<CommandBuffer> EndCommandBuffer();
|
||||||
|
|
||||||
|
/// Build the CommandBuffer, command after command.
|
||||||
|
/// This must be used between BeginCommandBuffer and EndCommandBuffer.
|
||||||
|
template<typename T, typename... Args>
|
||||||
|
ptrdiff_t AddCommand(Args&&... aArgs)
|
||||||
|
{
|
||||||
|
static_assert(IsBaseOf<DrawingCommand, T>::value,
|
||||||
|
"T must derive from DrawingCommand");
|
||||||
|
return mCommands->mStorage.Alloc<T>(Forward<Args>(aArgs)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasCommands() const { return !!mCommands; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
RefPtr<CommandBuffer> mCommands;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Stores multiple commands to be executed sequencially.
|
||||||
|
class DrawingTask : public Task {
|
||||||
|
public:
|
||||||
|
DrawingTask(MultiThreadedTaskQueue* aTaskQueue,
|
||||||
|
DrawTarget* aTarget,
|
||||||
|
IntPoint aOffset,
|
||||||
|
SyncObject* aStart);
|
||||||
|
|
||||||
|
~DrawingTask();
|
||||||
|
|
||||||
|
virtual TaskStatus Run() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/// Runs the tasks's destructors and resets the buffer.
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
std::vector<ptrdiff_t> mCommandOffsets;
|
||||||
|
RefPtr<CommandBuffer> mCommandBuffer;
|
||||||
|
uint32_t mCursor;
|
||||||
|
|
||||||
|
RefPtr<DrawTarget> mDrawTarget;
|
||||||
|
IntPoint mOffset;
|
||||||
|
|
||||||
|
friend class DrawingTaskBuilder;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generates DrawingTask objects.
|
||||||
|
///
|
||||||
|
/// The builder is a separate object to ensure that commands are not added to a
|
||||||
|
/// submitted DrawingTask.
|
||||||
|
class DrawingTaskBuilder {
|
||||||
|
public:
|
||||||
|
DrawingTaskBuilder();
|
||||||
|
|
||||||
|
~DrawingTaskBuilder();
|
||||||
|
|
||||||
|
/// Allocates a DrawingTask.
|
||||||
|
///
|
||||||
|
/// call this method before starting to add commands.
|
||||||
|
void BeginDrawingTask(MultiThreadedTaskQueue* aTaskQueue,
|
||||||
|
DrawTarget* aTarget, IntPoint aOffset,
|
||||||
|
SyncObject* aStart = nullptr);
|
||||||
|
|
||||||
|
/// Build the DrawingTask, command after command.
|
||||||
|
/// This must be used between BeginDrawingTask and EndDrawingTask.
|
||||||
|
void AddCommand(ptrdiff_t offset)
|
||||||
|
{
|
||||||
|
MOZ_ASSERT(mTask);
|
||||||
|
mTask->mCommandOffsets.push_back(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finalizes and returns the command buffer.
|
||||||
|
///
|
||||||
|
/// If aCompletion is not null, the sync object will be signaled after the
|
||||||
|
/// task buffer is destroyed (and after the destructor of the tasks have run).
|
||||||
|
/// In most cases this means after the completion of all tasks in the task buffer,
|
||||||
|
/// but also when the task buffer is destroyed due to an error.
|
||||||
|
DrawingTask* EndDrawingTask(CommandBuffer* aCmdBuffer, SyncObject* aCompletion = nullptr);
|
||||||
|
|
||||||
|
/// Returns true between BeginDrawingTask and EndDrawingTask, false otherwise.
|
||||||
|
bool HasDrawingTask() const { return !!mTask; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
DrawingTask* mTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#endif
|
|
@ -127,6 +127,7 @@ UNIFIED_SOURCES += [
|
||||||
'DataSourceSurface.cpp',
|
'DataSourceSurface.cpp',
|
||||||
'DataSurfaceHelpers.cpp',
|
'DataSurfaceHelpers.cpp',
|
||||||
'DrawEventRecorder.cpp',
|
'DrawEventRecorder.cpp',
|
||||||
|
'DrawingTask.cpp',
|
||||||
'DrawTarget.cpp',
|
'DrawTarget.cpp',
|
||||||
'DrawTargetCairo.cpp',
|
'DrawTargetCairo.cpp',
|
||||||
'DrawTargetCapture.cpp',
|
'DrawTargetCapture.cpp',
|
||||||
|
|
Загрузка…
Ссылка в новой задаче