fonts: Set up remote glyph caching to push fonts.
Currently the SkStrikeClient is designed to pull fonts from the server on demand, and to pre-fetch a batched request by analyzing the ops using a SkTextBlobCacheDiffCanvas. This change modifies the design to support a push based model, where the server pushes fonts required by the client and sets up the requisite SkGlyphCaches on the client prior to rasterizing the ops. This model still relies on the SkTextBlobCacheDiffCanvas for analyzing the glyphs required for rasterizing an op. The glyph caches required for raster are locked and missing glyphs to be sent to the client are tracked by the SkStrikeServer. The embedder can serialize this font data at any point, but must ensure that this data is deserialized by the SkStrikeClient at the remote end, before rasterizing any ops analyzed prior to serialization. Any refs on the caches are released once the font data is serialized by the server. The locking of glyph caches relies on the embedder providing discardable handles. These handles can be created on the server and serialized to be sent to the client, and map to an instance of SkGlyphCache. This allows the server to control the lifetime of the caches on the client. Bug: skia:7515 Change-Id: Id39f346b47b60899778404bbd0429ee811d0e53b Reviewed-on: https://skia-review.googlesource.com/120283 Commit-Queue: Khusal Sagar <khushalsagar@chromium.org> Reviewed-by: Herb Derby <herb@google.com>
This commit is contained in:
parent
e618eee833
commit
101d56359a
@ -226,6 +226,7 @@ tests_sources = [
|
||||
"$_tests/SkNxTest.cpp",
|
||||
"$_tests/SkPEGTest.cpp",
|
||||
"$_tests/SkRasterPipelineTest.cpp",
|
||||
"$_tests/SkRemoteGlyphCacheTest.cpp",
|
||||
"$_tests/SkResourceCacheTest.cpp",
|
||||
"$_tests/SkSharedMutexTest.cpp",
|
||||
"$_tests/SkSLErrorTest.cpp",
|
||||
|
@ -9,13 +9,51 @@
|
||||
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
#include "SkDevice.h"
|
||||
#include "SkFindAndPlaceGlyph.h"
|
||||
#include "SkStrikeCache.h"
|
||||
#include "SkTextBlobRunIterator.h"
|
||||
#include "SkTraceEvent.h"
|
||||
#include "SkTypeface_remote.h"
|
||||
|
||||
static SkDescriptor* auto_descriptor_from_desc(const SkDescriptor* source_desc,
|
||||
SkFontID font_id,
|
||||
SkAutoDescriptor* ad) {
|
||||
ad->reset(source_desc->getLength());
|
||||
auto* desc = ad->getDesc();
|
||||
desc->init();
|
||||
|
||||
// Rec.
|
||||
{
|
||||
uint32_t size;
|
||||
auto ptr = source_desc->findEntry(kRec_SkDescriptorTag, &size);
|
||||
SkScalerContextRec rec;
|
||||
std::memcpy(&rec, ptr, size);
|
||||
rec.fFontID = font_id;
|
||||
desc->addEntry(kRec_SkDescriptorTag, sizeof(rec), &rec);
|
||||
}
|
||||
|
||||
// Path effect.
|
||||
{
|
||||
uint32_t size;
|
||||
auto ptr = source_desc->findEntry(kPathEffect_SkDescriptorTag, &size);
|
||||
if (ptr) desc->addEntry(kPathEffect_SkDescriptorTag, size, ptr);
|
||||
}
|
||||
|
||||
// Mask filter.
|
||||
{
|
||||
uint32_t size;
|
||||
auto ptr = source_desc->findEntry(kMaskFilter_SkDescriptorTag, &size);
|
||||
if (ptr) desc->addEntry(kMaskFilter_SkDescriptorTag, size, ptr);
|
||||
}
|
||||
|
||||
desc->computeChecksum();
|
||||
return desc;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
class ArraySlice final : public std::tuple<const T*, size_t> {
|
||||
public:
|
||||
@ -26,7 +64,6 @@ public:
|
||||
const T* begin() {
|
||||
return this->data();
|
||||
}
|
||||
|
||||
const T* end() {
|
||||
return &this->data()[this->size()];
|
||||
}
|
||||
@ -46,28 +83,32 @@ private:
|
||||
|
||||
// -- Serializer ----------------------------------------------------------------------------------
|
||||
|
||||
static size_t pad(size_t size, size_t alignment) {
|
||||
return (size + (alignment - 1)) & ~(alignment - 1);
|
||||
}
|
||||
size_t pad(size_t size, size_t alignment) { return (size + (alignment - 1)) & ~(alignment - 1); }
|
||||
|
||||
// N.B. pointers are only valid until the next call.
|
||||
class Serializer {
|
||||
public:
|
||||
Serializer(std::vector<uint8_t>* buffer) : fBuffer{buffer} { }
|
||||
|
||||
template <typename T>
|
||||
T* push_back(const T& data) {
|
||||
auto result = allocate(sizeof(T), alignof(T));
|
||||
return new (result) T(data);
|
||||
}
|
||||
|
||||
template <typename T, typename... Args>
|
||||
T* emplace_back(Args&& ... args) {
|
||||
T* emplace(Args&&... args) {
|
||||
auto result = allocate(sizeof(T), alignof(T));
|
||||
return new (result) T{std::forward<Args>(args)...};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void write(const T& data) {
|
||||
T* result = (T*)allocate(sizeof(T), alignof(T));
|
||||
memcpy(result, &data, sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* allocate() {
|
||||
T* result = (T*)allocate(sizeof(T), alignof(T));
|
||||
return result;
|
||||
}
|
||||
|
||||
void writeDescriptor(const SkDescriptor& desc) {
|
||||
write(desc.getLength());
|
||||
auto result = allocate(desc.getLength(), alignof(SkDescriptor));
|
||||
memcpy(result, &desc, desc.getLength());
|
||||
}
|
||||
@ -89,86 +130,69 @@ private:
|
||||
};
|
||||
|
||||
// -- Deserializer -------------------------------------------------------------------------------
|
||||
|
||||
// Note that the Deserializer is reading untrusted data, we need to guard against invalid data.
|
||||
class Deserializer {
|
||||
public:
|
||||
Deserializer(const SkData& buffer) : fBuffer{buffer} { }
|
||||
Deserializer(const volatile char* memory, size_t memorySize)
|
||||
: fMemory(memory), fMemorySize(memorySize) {}
|
||||
|
||||
template <typename T>
|
||||
T* read() {
|
||||
size_t padded = pad(fCursor, alignof(T));
|
||||
fCursor = padded + sizeof(T);
|
||||
auto data = (uint8_t*)fBuffer.data();
|
||||
return (T*)&data[padded];
|
||||
bool read(T* val) {
|
||||
auto* result = this->ensureAtLeast(sizeof(T), alignof(T));
|
||||
if (!result) return false;
|
||||
|
||||
memcpy(val, const_cast<const char*>(result), sizeof(T));
|
||||
return true;
|
||||
}
|
||||
|
||||
SkDescriptor* readDescriptor() {
|
||||
size_t padded = pad(fCursor, alignof(SkDescriptor));
|
||||
auto data = (uint8_t*)fBuffer.data();
|
||||
SkDescriptor* result = (SkDescriptor*)&data[padded];
|
||||
fCursor = padded + result->getLength();
|
||||
return result;
|
||||
bool readDescriptor(SkAutoDescriptor* ad) {
|
||||
uint32_t desc_length = 0u;
|
||||
if (!read<uint32_t>(&desc_length)) return false;
|
||||
|
||||
auto* result = this->ensureAtLeast(desc_length, alignof(SkDescriptor));
|
||||
if (!result) return false;
|
||||
|
||||
ad->reset(desc_length);
|
||||
memcpy(ad->getDesc(), const_cast<const char*>(result), desc_length);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
ArraySlice<T> readArray(int count) {
|
||||
size_t padded = pad(fCursor, alignof(T));
|
||||
size_t size = count * sizeof(T);
|
||||
auto data = (uint8_t*)fBuffer.data();
|
||||
const T* base = (const T*)&data[padded];
|
||||
const T* base = (const T*)this->ensureAtLeast(size, alignof(T));
|
||||
if (!base) return ArraySlice<T>();
|
||||
|
||||
ArraySlice<T> result = ArraySlice<T>{base, (uint32_t)count};
|
||||
fCursor = padded + size;
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t size() {return fCursor;}
|
||||
|
||||
private:
|
||||
const SkData& fBuffer;
|
||||
size_t fCursor{0};
|
||||
const volatile char* ensureAtLeast(size_t size, size_t alignment) {
|
||||
size_t padded = pad(fBytesRead, alignment);
|
||||
|
||||
// Not enough data
|
||||
if (padded + size > fMemorySize) return nullptr;
|
||||
|
||||
auto* result = fMemory + padded;
|
||||
fBytesRead = padded + size;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Note that we read each piece of memory only once to guard against TOCTOU violations.
|
||||
const volatile char* fMemory;
|
||||
size_t fMemorySize;
|
||||
size_t fBytesRead = 0u;
|
||||
};
|
||||
|
||||
|
||||
// -- SkStrikeCacheDifferenceSpec ------------------------------------------------------------------
|
||||
|
||||
SkStrikeDifferences::SkStrikeDifferences(
|
||||
SkFontID typefaceID, std::unique_ptr<SkDescriptor> desc)
|
||||
: fTypefaceID{typefaceID}
|
||||
, fDesc{std::move(desc)} { }
|
||||
|
||||
void SkStrikeDifferences::add(uint16_t glyphID, SkIPoint pos) {
|
||||
SkPackedGlyphID packedGlyphID{glyphID, pos.x(), pos.y()};
|
||||
fGlyphIDs->add(packedGlyphID);
|
||||
}
|
||||
|
||||
SkStrikeDifferences& SkStrikeCacheDifferenceSpec::findStrikeDifferences(
|
||||
const SkDescriptor& desc, SkFontID typefaceID)
|
||||
{
|
||||
auto mapIter = fDescriptorToDifferencesMap.find(&desc);
|
||||
if (mapIter == fDescriptorToDifferencesMap.end()) {
|
||||
auto newDesc = desc.copy();
|
||||
auto newDescPtr = newDesc.get();
|
||||
SkStrikeDifferences strikeDiffs{typefaceID, std::move(newDesc)};
|
||||
|
||||
mapIter = fDescriptorToDifferencesMap.emplace_hint(
|
||||
mapIter, newDescPtr, std::move(strikeDiffs));
|
||||
size_t SkDescriptorMapOperators::operator()(const SkDescriptor* key) const {
|
||||
return key->getChecksum();
|
||||
}
|
||||
|
||||
return mapIter->second;
|
||||
}
|
||||
|
||||
template <typename PerStrike, typename PerGlyph>
|
||||
void SkStrikeCacheDifferenceSpec::iterateDifferences(PerStrike perStrike, PerGlyph perGlyph) const {
|
||||
for (auto& i : fDescriptorToDifferencesMap) {
|
||||
auto strikeDiff = &i.second;
|
||||
perStrike(strikeDiff->fTypefaceID,
|
||||
*strikeDiff->fDesc,
|
||||
strikeDiff->fGlyphIDs->count());
|
||||
strikeDiff->fGlyphIDs->foreach([&](SkPackedGlyphID id) {
|
||||
perGlyph(id);
|
||||
});
|
||||
bool SkDescriptorMapOperators::operator()(const SkDescriptor* lhs,
|
||||
const SkDescriptor* rhs) const {
|
||||
return *lhs == *rhs;
|
||||
}
|
||||
}
|
||||
|
||||
// -- TrackLayerDevice -----------------------------------------------------------------------------
|
||||
class TrackLayerDevice : public SkNoPixelsDevice {
|
||||
@ -182,17 +206,18 @@ public:
|
||||
};
|
||||
|
||||
// -- SkTextBlobCacheDiffCanvas -------------------------------------------------------------------
|
||||
SkTextBlobCacheDiffCanvas::SkTextBlobCacheDiffCanvas(
|
||||
int width, int height,
|
||||
const SkMatrix& deviceMatrix,
|
||||
const SkSurfaceProps& props,
|
||||
SkScalerContextFlags flags,
|
||||
SkStrikeCacheDifferenceSpec* strikeDiffs)
|
||||
SkTextBlobCacheDiffCanvas::SkTextBlobCacheDiffCanvas(int width, int height,
|
||||
const SkMatrix& deviceMatrix,
|
||||
const SkSurfaceProps& props,
|
||||
SkStrikeServer* strikeSever)
|
||||
: SkNoDrawCanvas{new TrackLayerDevice{SkIRect::MakeWH(width, height), props}}
|
||||
, fDeviceMatrix{deviceMatrix}
|
||||
, fSurfaceProps{props}
|
||||
, fScalerContextFlags{flags}
|
||||
, fStrikeCacheDiff{strikeDiffs} { }
|
||||
, fStrikeServer{strikeSever} {
|
||||
SkASSERT(fStrikeServer);
|
||||
}
|
||||
|
||||
SkTextBlobCacheDiffCanvas::~SkTextBlobCacheDiffCanvas() = default;
|
||||
|
||||
SkCanvas::SaveLayerStrategy SkTextBlobCacheDiffCanvas::getSaveLayerStrategy(
|
||||
const SaveLayerRec&rec)
|
||||
@ -235,6 +260,10 @@ void SkTextBlobCacheDiffCanvas::processLooper(
|
||||
}
|
||||
}
|
||||
|
||||
#define FAIL_AND_RETURN \
|
||||
SkDEBUGFAIL("Failed to process glyph run"); \
|
||||
return;
|
||||
|
||||
void SkTextBlobCacheDiffCanvas::processGlyphRun(
|
||||
const SkPoint& position,
|
||||
const SkTextBlobRunIterator& it,
|
||||
@ -242,20 +271,24 @@ void SkTextBlobCacheDiffCanvas::processGlyphRun(
|
||||
{
|
||||
|
||||
if (runPaint.getTextEncoding() != SkPaint::TextEncoding::kGlyphID_TextEncoding) {
|
||||
return;
|
||||
TRACE_EVENT0("skia", "kGlyphID_TextEncoding");
|
||||
FAIL_AND_RETURN
|
||||
}
|
||||
|
||||
// All other alignment modes need the glyph advances. Use the slow drawing mode.
|
||||
if (runPaint.getTextAlign() != SkPaint::kLeft_Align) {
|
||||
return;
|
||||
TRACE_EVENT0("skia", "kLeft_Align");
|
||||
FAIL_AND_RETURN
|
||||
}
|
||||
|
||||
using PosFn = SkPoint(*)(int index, const SkScalar* pos);
|
||||
PosFn posFn;
|
||||
switch (it.positioning()) {
|
||||
case SkTextBlob::kDefault_Positioning:
|
||||
case SkTextBlob::kDefault_Positioning: {
|
||||
// Default positioning needs advances. Can't do that.
|
||||
return;
|
||||
TRACE_EVENT0("skia", "kDefault_Positioning");
|
||||
FAIL_AND_RETURN
|
||||
}
|
||||
|
||||
case SkTextBlob::kHorizontal_Positioning:
|
||||
posFn = [](int index, const SkScalar* pos) {
|
||||
@ -278,7 +311,8 @@ void SkTextBlobCacheDiffCanvas::processGlyphRun(
|
||||
SkMatrix blobMatrix{fDeviceMatrix};
|
||||
blobMatrix.preConcat(this->getTotalMatrix());
|
||||
if (blobMatrix.hasPerspective()) {
|
||||
return;
|
||||
TRACE_EVENT0("skia", "hasPerspective");
|
||||
FAIL_AND_RETURN
|
||||
}
|
||||
blobMatrix.preTranslate(position.x(), position.y());
|
||||
|
||||
@ -315,19 +349,21 @@ void SkTextBlobCacheDiffCanvas::processGlyphRun(
|
||||
SK_ABORT("Bad matrix.");
|
||||
}
|
||||
|
||||
SkAutoDescriptor ad;
|
||||
SkScalerContextRec rec;
|
||||
SkScalerContextEffects effects;
|
||||
|
||||
// TODO(crbug.com/831354): The typeface proxy on the client does not replicate the
|
||||
// filtering done by the typeface on the server.
|
||||
SkScalerContext::MakeRecAndEffects(runPaint, &fSurfaceProps, &runMatrix,
|
||||
fScalerContextFlags, &rec, &effects);
|
||||
SkScalerContextFlags::kFakeGammaAndBoostContrast, &rec,
|
||||
&effects);
|
||||
|
||||
auto desc = SkScalerContext::AutoDescriptorGivenRecAndEffects(rec, effects, &ad);
|
||||
TRACE_EVENT1("skia", "RecForDesc", "rec", TRACE_STR_COPY(rec.dump().c_str()));
|
||||
auto desc = SkScalerContext::DescriptorGivenRecAndEffects(rec, effects);
|
||||
auto* glyphCacheState = static_cast<SkStrikeServer*>(fStrikeServer)
|
||||
->getOrCreateCache(runPaint.getTypeface(), std::move(desc));
|
||||
SkASSERT(glyphCacheState);
|
||||
|
||||
auto typefaceID = SkTypefaceProxy::DownCast(runPaint.getTypeface())->remoteTypefaceID();
|
||||
auto& diffs = fStrikeCacheDiff->findStrikeDifferences(*desc, typefaceID);
|
||||
|
||||
auto cache = SkStrikeCache::FindStrikeExclusive(*desc);
|
||||
bool isSubpixel = SkToBool(rec.fFlags & SkScalerContext::kSubpixelPositioning_Flag);
|
||||
SkAxisAlignment axisAlignment = SkAxisAlignment::kNone_SkAxisAlignment;
|
||||
if (it.positioning() == SkTextBlob::kHorizontal_Positioning) {
|
||||
@ -342,392 +378,323 @@ void SkTextBlobCacheDiffCanvas::processGlyphRun(
|
||||
subPixelPos = SkFindAndPlaceGlyph::SubpixelAlignment(axisAlignment, glyphPos);
|
||||
}
|
||||
|
||||
if (cache &&
|
||||
cache->isGlyphCached(glyphs[index], subPixelPos.x(), subPixelPos.y())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
diffs.add(glyphs[index], subPixelPos);
|
||||
glyphCacheState->addGlyph(runPaint.getTypeface(),
|
||||
effects,
|
||||
SkPackedGlyphID(glyphs[index], subPixelPos.x(), subPixelPos.y()));
|
||||
}
|
||||
}
|
||||
|
||||
// Op code semantics:
|
||||
// * FontMetrics - (SkFontID, SkDescriptor) -> SkPaint::FontMetrics
|
||||
// * GlyphPath - (SkFontID, SkDescriptor, SkPackedGlyphID) -> SkPath
|
||||
// * GlyphMetricsAndImage - (SkFontID, SkDescriptor, SkPackedGlyphID) -> (SkGlyph, <image bits>)
|
||||
// * PrepopulateCache - StrikeCacheDifferenceSpec -> StrikeCacheDifferenceData
|
||||
|
||||
enum class OpCode : int32_t {
|
||||
kFontMetrics = 0,
|
||||
kGlyphPath = 1,
|
||||
kGlyphMetricsAndImage = 2,
|
||||
kPrepopulateCache = 3,
|
||||
};
|
||||
|
||||
struct StrikeDiffHeader {
|
||||
StrikeDiffHeader() {}
|
||||
StrikeDiffHeader(int strikeCount_) : strikeCount{strikeCount_} {}
|
||||
int strikeCount;
|
||||
};
|
||||
|
||||
struct StrikeSpec {
|
||||
StrikeSpec(SkFontID typefaceID_, uint32_t descLength_, int glyphCount_)
|
||||
StrikeSpec() {}
|
||||
StrikeSpec(SkFontID typefaceID_, size_t glyphCount_, SkDiscardableHandleId discardableHandleId_)
|
||||
: typefaceID{typefaceID_}
|
||||
, descLength{descLength_}
|
||||
, glyphCount{glyphCount_} { }
|
||||
SkFontID typefaceID;
|
||||
uint32_t descLength;
|
||||
int glyphCount;
|
||||
, glyphCount{glyphCount_}
|
||||
, discardableHandleId(discardableHandleId_) {}
|
||||
SkFontID typefaceID = 0u;
|
||||
size_t glyphCount = 0u;
|
||||
SkDiscardableHandleId discardableHandleId = 0u;
|
||||
/* desc */
|
||||
/* n X (glyphs ids) */
|
||||
};
|
||||
|
||||
struct WireTypeface {
|
||||
WireTypeface() = default;
|
||||
WireTypeface(SkFontID typeface_id, int glyph_count, SkFontStyle style, bool is_fixed)
|
||||
: typefaceID(typeface_id), glyphCount(glyph_count), style(style), isFixed(is_fixed) {}
|
||||
|
||||
// std::thread::id thread_id; // TODO:need to figure a good solution
|
||||
SkFontID typefaceID;
|
||||
int glyphCount;
|
||||
SkFontStyle style;
|
||||
bool isFixed;
|
||||
};
|
||||
|
||||
class Op {
|
||||
// SkStrikeServer -----------------------------------------
|
||||
|
||||
SkStrikeServer::SkStrikeServer(DiscardableHandleManager* discardableHandleManager)
|
||||
: fDiscardableHandleManager(discardableHandleManager) {
|
||||
SkASSERT(fDiscardableHandleManager);
|
||||
}
|
||||
|
||||
SkStrikeServer::~SkStrikeServer() = default;
|
||||
|
||||
sk_sp<SkData> SkStrikeServer::serializeTypeface(SkTypeface* tf) {
|
||||
WireTypeface wire(SkTypeface::UniqueID(tf), tf->countGlyphs(), tf->fontStyle(),
|
||||
tf->isFixedPitch());
|
||||
return SkData::MakeWithCopy(&wire, sizeof(wire));
|
||||
}
|
||||
|
||||
void SkStrikeServer::writeStrikeData(std::vector<uint8_t>* memory) {
|
||||
if (fLockedDescs.empty() && fTypefacesToSend.empty()) return;
|
||||
|
||||
Serializer serializer(memory);
|
||||
serializer.emplace<size_t>(fTypefacesToSend.size());
|
||||
for (const auto& tf : fTypefacesToSend) serializer.write<WireTypeface>(tf);
|
||||
fTypefacesToSend.clear();
|
||||
|
||||
serializer.emplace<size_t>(fLockedDescs.size());
|
||||
for (const auto* desc : fLockedDescs) {
|
||||
auto it = fRemoteGlyphStateMap.find(desc);
|
||||
SkASSERT(it != fRemoteGlyphStateMap.end());
|
||||
|
||||
// TODO: This is unnecessary, write only the descs which has any glyphs
|
||||
// to send. It was getting awkward to write the size after writing the
|
||||
// descs because the vector reallocs.
|
||||
serializer.emplace<bool>(it->second->has_pending_glyphs());
|
||||
if (!it->second->has_pending_glyphs()) continue;
|
||||
|
||||
it->second->writePendingGlyphs(&serializer);
|
||||
}
|
||||
fLockedDescs.clear();
|
||||
}
|
||||
|
||||
SkStrikeServer::SkGlyphCacheState* SkStrikeServer::getOrCreateCache(
|
||||
SkTypeface* tf, std::unique_ptr<SkDescriptor> desc) {
|
||||
SkASSERT(desc);
|
||||
|
||||
// Already locked.
|
||||
if (fLockedDescs.find(desc.get()) != fLockedDescs.end()) {
|
||||
auto it = fRemoteGlyphStateMap.find(desc.get());
|
||||
SkASSERT(it != fRemoteGlyphStateMap.end());
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
// Try to lock.
|
||||
auto it = fRemoteGlyphStateMap.find(desc.get());
|
||||
if (it != fRemoteGlyphStateMap.end()) {
|
||||
bool locked = fDiscardableHandleManager->lockHandle(it->second->discardable_handle_id());
|
||||
if (locked) {
|
||||
fLockedDescs.insert(it->first);
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
// If the lock failed, the entry was deleted on the client. Remove our
|
||||
// tracking.
|
||||
fRemoteGlyphStateMap.erase(it);
|
||||
}
|
||||
|
||||
const SkFontID typeface_id = tf->uniqueID();
|
||||
if (!fCachedTypefaces.contains(typeface_id)) {
|
||||
fCachedTypefaces.add(typeface_id);
|
||||
fTypefacesToSend.emplace_back(typeface_id, tf->countGlyphs(), tf->fontStyle(),
|
||||
tf->isFixedPitch());
|
||||
}
|
||||
|
||||
auto* desc_ptr = desc.get();
|
||||
auto new_handle = fDiscardableHandleManager->createHandle();
|
||||
auto cache_state = skstd::make_unique<SkGlyphCacheState>(std::move(desc), new_handle);
|
||||
auto* cache_state_ptr = cache_state.get();
|
||||
|
||||
fLockedDescs.insert(desc_ptr);
|
||||
fRemoteGlyphStateMap[desc_ptr] = std::move(cache_state);
|
||||
return cache_state_ptr;
|
||||
}
|
||||
|
||||
SkStrikeServer::SkGlyphCacheState::SkGlyphCacheState(std::unique_ptr<SkDescriptor> desc,
|
||||
uint32_t discardable_handle_id)
|
||||
: fDesc(std::move(desc)), fDiscardableHandleId(discardable_handle_id) {
|
||||
SkASSERT(fDesc);
|
||||
}
|
||||
|
||||
SkStrikeServer::SkGlyphCacheState::~SkGlyphCacheState() = default;
|
||||
|
||||
void SkStrikeServer::SkGlyphCacheState::addGlyph(SkTypeface* typeface,
|
||||
const SkScalerContextEffects& effects,
|
||||
SkPackedGlyphID glyph) {
|
||||
// Already cached.
|
||||
if (fCachedGlyphs.contains(glyph)) return;
|
||||
|
||||
// Serialize and cache. Also create the scalar context to use when serializing
|
||||
// this glyph.
|
||||
fCachedGlyphs.add(glyph);
|
||||
fPendingGlyphs.push_back(glyph);
|
||||
if (!fContext) fContext = typeface->createScalerContext(effects, fDesc.get(), false);
|
||||
}
|
||||
|
||||
void SkStrikeServer::SkGlyphCacheState::writePendingGlyphs(Serializer* serializer) {
|
||||
// Write the desc.
|
||||
serializer->emplace<StrikeSpec>(fContext->getTypeface()->uniqueID(), fPendingGlyphs.size(),
|
||||
fDiscardableHandleId);
|
||||
serializer->writeDescriptor(*fDesc.get());
|
||||
|
||||
// Write FontMetrics.
|
||||
SkPaint::FontMetrics fontMetrics;
|
||||
fContext->getFontMetrics(&fontMetrics);
|
||||
serializer->write<SkPaint::FontMetrics>(fontMetrics);
|
||||
|
||||
// Write Glyphs.
|
||||
for (const auto& glyphID : fPendingGlyphs) {
|
||||
auto glyph = serializer->emplace<SkGlyph>();
|
||||
glyph->initWithGlyphID(glyphID);
|
||||
fContext->getMetrics(glyph);
|
||||
auto imageSize = glyph->computeImageSize();
|
||||
glyph->fPathData = nullptr;
|
||||
glyph->fImage = nullptr;
|
||||
|
||||
if (imageSize > 0) {
|
||||
// Since the allocateArray can move glyph, make one that stays in one place.
|
||||
SkGlyph stationaryGlyph = *glyph;
|
||||
stationaryGlyph.fImage = serializer->allocateArray<uint8_t>(imageSize);
|
||||
fContext->getImage(stationaryGlyph);
|
||||
}
|
||||
}
|
||||
|
||||
// Note that we reset the context after serializing pending glyphs since we
|
||||
// don't want to extend the lifetime of the typeface.
|
||||
fPendingGlyphs.clear();
|
||||
fContext.reset();
|
||||
}
|
||||
|
||||
// SkStrikeClient -----------------------------------------
|
||||
|
||||
class SkStrikeClient::DiscardableStrikePinner : public SkStrikePinner {
|
||||
public:
|
||||
Op(OpCode opCode, SkFontID typefaceId, const SkScalerContextRec& rec)
|
||||
: opCode{opCode}
|
||||
, typefaceId{typefaceId}
|
||||
, descriptor{rec} { }
|
||||
const OpCode opCode;
|
||||
const SkFontID typefaceId;
|
||||
const SkScalerContextRecDescriptor descriptor;
|
||||
union {
|
||||
// kGlyphPath and kGlyphMetricsAndImage
|
||||
SkPackedGlyphID glyphID;
|
||||
// kPrepopulateCache
|
||||
StrikeDiffHeader strikeSpecHeader;
|
||||
};
|
||||
DiscardableStrikePinner(SkDiscardableHandleId discardableHandleId,
|
||||
sk_sp<DiscardableHandleManager> manager)
|
||||
: fDiscardableHandleId(discardableHandleId), fManager(std::move(manager)) {}
|
||||
|
||||
~DiscardableStrikePinner() override = default;
|
||||
bool canDelete() override { return fManager->deleteHandle(fDiscardableHandleId); }
|
||||
|
||||
private:
|
||||
const SkDiscardableHandleId fDiscardableHandleId;
|
||||
sk_sp<DiscardableHandleManager> fManager;
|
||||
};
|
||||
|
||||
size_t SkStrikeCacheDifferenceSpec::sizeBytes() const {
|
||||
size_t sum = sizeof(Op) + sizeof(StrikeDiffHeader);
|
||||
for (auto& pair : fDescriptorToDifferencesMap) {
|
||||
const auto& strike = pair.second;
|
||||
sum += sizeof(StrikeSpec)
|
||||
+ strike.fDesc->getLength()
|
||||
+ strike.fGlyphIDs->count() * sizeof(SkPackedGlyphID);
|
||||
SkStrikeClient::SkStrikeClient(sk_sp<DiscardableHandleManager> discardableManager)
|
||||
: fDiscardableHandleManager(std::move(discardableManager)) {}
|
||||
|
||||
SkStrikeClient::~SkStrikeClient() = default;
|
||||
|
||||
#define READ_FAILURE \
|
||||
{ \
|
||||
SkDEBUGFAIL("Bad serialization"); \
|
||||
return false; \
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
static void write_strikes_spec(const SkStrikeCacheDifferenceSpec &spec,
|
||||
Serializer* serializer) {
|
||||
serializer->emplace_back<Op>(OpCode::kPrepopulateCache, SkFontID{0}, SkScalerContextRec{});
|
||||
bool SkStrikeClient::readStrikeData(const volatile void* memory, size_t memorySize) {
|
||||
SkASSERT(memorySize != 0u);
|
||||
Deserializer deserializer(static_cast<const volatile char*>(memory), memorySize);
|
||||
|
||||
serializer->emplace_back<StrikeDiffHeader>(spec.strikeCount());
|
||||
size_t typefaceSize = 0u;
|
||||
if (!deserializer.read<size_t>(&typefaceSize)) READ_FAILURE
|
||||
|
||||
auto perStrike = [serializer](SkFontID typefaceID, const SkDescriptor& desc, int glyphCount) {
|
||||
serializer->emplace_back<StrikeSpec>(typefaceID, desc.getLength(), glyphCount);
|
||||
serializer->writeDescriptor(desc);
|
||||
};
|
||||
for (size_t i = 0; i < typefaceSize; ++i) {
|
||||
WireTypeface wire;
|
||||
if (!deserializer.read<WireTypeface>(&wire)) READ_FAILURE
|
||||
|
||||
auto perGlyph = [serializer](SkPackedGlyphID glyphID) {
|
||||
serializer->push_back<SkPackedGlyphID>(glyphID);
|
||||
};
|
||||
// TODO(khushalsagar): The typeface no longer needs a reference to the
|
||||
// SkStrikeClient, since all needed glyphs must have been pushed before
|
||||
// raster.
|
||||
addTypeface(wire);
|
||||
}
|
||||
|
||||
spec.iterateDifferences(perStrike, perGlyph);
|
||||
}
|
||||
size_t strikeCount = 0u;
|
||||
if (!deserializer.read<size_t>(&strikeCount)) READ_FAILURE
|
||||
|
||||
for (size_t i = 0; i < strikeCount; ++i) {
|
||||
bool has_glyphs = false;
|
||||
if (!deserializer.read<bool>(&has_glyphs)) READ_FAILURE
|
||||
|
||||
if (!has_glyphs) continue;
|
||||
|
||||
StrikeSpec spec;
|
||||
if (!deserializer.read<StrikeSpec>(&spec)) READ_FAILURE
|
||||
|
||||
SkAutoDescriptor sourceAd;
|
||||
if (!deserializer.readDescriptor(&sourceAd)) READ_FAILURE
|
||||
|
||||
static void read_strikes_spec_write_strikes_data(
|
||||
Deserializer* deserializer, Serializer* serializer, SkStrikeServer* server)
|
||||
{
|
||||
// Don't start because the op started this deserialization.
|
||||
auto header = deserializer->read<StrikeDiffHeader>();
|
||||
serializer->push_back<StrikeDiffHeader>(*header);
|
||||
for (int i = 0; i < header->strikeCount; i++) {
|
||||
auto spec = deserializer->read<StrikeSpec>();
|
||||
auto desc = deserializer->readDescriptor();
|
||||
serializer->push_back<StrikeSpec>(*spec);
|
||||
serializer->writeDescriptor(*desc);
|
||||
SkScalerContextRecDescriptor recDesc{*desc};
|
||||
auto scaler = server->generateScalerContext(recDesc, spec->typefaceID);
|
||||
SkPaint::FontMetrics fontMetrics;
|
||||
scaler->getFontMetrics(&fontMetrics);
|
||||
serializer->push_back<SkPaint::FontMetrics>(fontMetrics);
|
||||
auto glyphIDs = deserializer->readArray<SkPackedGlyphID>(spec->glyphCount);
|
||||
for (auto glyphID : glyphIDs) {
|
||||
auto glyph = serializer->emplace_back<SkGlyph>();
|
||||
glyph->initWithGlyphID(glyphID);
|
||||
scaler->getMetrics(glyph);
|
||||
auto imageSize = glyph->computeImageSize();
|
||||
glyph->fPathData = nullptr;
|
||||
glyph->fImage = nullptr;
|
||||
if (!deserializer.read<SkPaint::FontMetrics>(&fontMetrics)) READ_FAILURE
|
||||
|
||||
if (imageSize > 0) {
|
||||
// Since the allocateArray can move glyph, make one that stays in one place.
|
||||
SkGlyph stationaryGlyph = *glyph;
|
||||
stationaryGlyph.fImage = serializer->allocateArray<uint8_t>(imageSize);
|
||||
scaler->getImage(stationaryGlyph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get the local typeface from remote fontID.
|
||||
auto* tf = fRemoteFontIdToTypeface.find(spec.typefaceID)->get();
|
||||
// Received strikes for a typeface which doesn't exist.
|
||||
if (!tf) READ_FAILURE
|
||||
|
||||
static void update_caches_from_strikes_data(SkStrikeClient *client,
|
||||
Deserializer *deserializer) {
|
||||
auto header = deserializer->read<StrikeDiffHeader>();
|
||||
for (int i = 0; i < header->strikeCount; i++) {
|
||||
auto spec = deserializer->read<StrikeSpec>();
|
||||
auto desc = deserializer->readDescriptor();
|
||||
auto fontMetrics = deserializer->read<SkPaint::FontMetrics>();
|
||||
auto tf = client->lookupTypeface(spec->typefaceID);
|
||||
// Replace the ContextRec in the desc from the server to create the client
|
||||
// side descriptor.
|
||||
// TODO: Can we do this in-place and re-compute checksum? Instead of a complete copy.
|
||||
SkAutoDescriptor ad;
|
||||
auto* client_desc = auto_descriptor_from_desc(sourceAd.getDesc(), tf->uniqueID(), &ad);
|
||||
|
||||
// TODO: implement effects handling.
|
||||
SkScalerContextEffects effects;
|
||||
auto strike = SkStrikeCache::FindStrikeExclusive(*desc);
|
||||
auto strike = SkStrikeCache::FindStrikeExclusive(*client_desc);
|
||||
if (strike == nullptr) {
|
||||
auto scaler = SkStrikeCache::CreateScalerContext(*desc, effects, *tf);
|
||||
strike = SkStrikeCache::CreateStrikeExclusive(*desc, std::move(scaler), fontMetrics);
|
||||
// Note that we don't need to deserialize the effects since we won't be generating any
|
||||
// glyphs here anyway, and the desc is still correct since it includes the serialized
|
||||
// effects.
|
||||
SkScalerContextEffects effects;
|
||||
auto scaler = SkStrikeCache::CreateScalerContext(*client_desc, effects, *tf);
|
||||
strike = SkStrikeCache::CreateStrikeExclusive(
|
||||
*client_desc, std::move(scaler), &fontMetrics,
|
||||
skstd::make_unique<DiscardableStrikePinner>(spec.discardableHandleId,
|
||||
fDiscardableHandleManager));
|
||||
}
|
||||
for (int j = 0; j < spec->glyphCount; j++) {
|
||||
auto glyph = deserializer->read<SkGlyph>();
|
||||
|
||||
for (size_t j = 0; j < spec.glyphCount; j++) {
|
||||
SkGlyph glyph;
|
||||
if (!deserializer.read<SkGlyph>(&glyph)) READ_FAILURE
|
||||
|
||||
ArraySlice<uint8_t> image;
|
||||
auto imageSize = glyph->computeImageSize();
|
||||
auto imageSize = glyph.computeImageSize();
|
||||
if (imageSize != 0) {
|
||||
image = deserializer->readArray<uint8_t>(imageSize);
|
||||
image = deserializer.readArray<uint8_t>(imageSize);
|
||||
if (!image.data()) READ_FAILURE
|
||||
}
|
||||
SkGlyph* allocatedGlyph = strike->getRawGlyphByID(glyph->getPackedID());
|
||||
*allocatedGlyph = *glyph;
|
||||
|
||||
SkGlyph* allocatedGlyph = strike->getRawGlyphByID(glyph.getPackedID());
|
||||
*allocatedGlyph = glyph;
|
||||
allocatedGlyph->allocImage(strike->getAlloc());
|
||||
memcpy(allocatedGlyph->fImage, image.data(), image.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- SkStrikeServer -------------------------------------------------------------------------------
|
||||
SkStrikeServer::SkStrikeServer() { }
|
||||
|
||||
SkStrikeServer::~SkStrikeServer() {
|
||||
printf("Strike server - ops: %d\n", fOpCount);
|
||||
}
|
||||
|
||||
void SkStrikeServer::serve(const SkData& inBuffer, std::vector<uint8_t>* outBuffer) {
|
||||
|
||||
fOpCount += 1;
|
||||
|
||||
Serializer serializer{outBuffer};
|
||||
Deserializer deserializer{inBuffer};
|
||||
Op* op = deserializer.read<Op>();
|
||||
|
||||
switch (op->opCode) {
|
||||
case OpCode::kFontMetrics : {
|
||||
auto scaler = this->generateScalerContext(op->descriptor, op->typefaceId);
|
||||
SkPaint::FontMetrics metrics;
|
||||
scaler->getFontMetrics(&metrics);
|
||||
serializer.push_back<SkPaint::FontMetrics>(metrics);
|
||||
break;
|
||||
}
|
||||
case OpCode::kGlyphPath : {
|
||||
auto sc = this->generateScalerContext(op->descriptor, op->typefaceId);
|
||||
// TODO: check for buffer overflow.
|
||||
SkPath path;
|
||||
if (sc->getPath(op->glyphID, &path)) {
|
||||
size_t pathSize = path.writeToMemory(nullptr);
|
||||
serializer.push_back<size_t>(pathSize);
|
||||
auto pathData = serializer.allocateArray<uint8_t>(pathSize);
|
||||
path.writeToMemory(pathData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OpCode::kGlyphMetricsAndImage : {
|
||||
auto scaler = this->generateScalerContext(op->descriptor, op->typefaceId);
|
||||
|
||||
auto glyph = serializer.emplace_back<SkGlyph>();
|
||||
// TODO: check for buffer overflow.
|
||||
glyph->initWithGlyphID(op->glyphID);
|
||||
scaler->getMetrics(glyph);
|
||||
auto imageSize = glyph->computeImageSize();
|
||||
glyph->fPathData = nullptr;
|
||||
glyph->fImage = nullptr;
|
||||
if (imageSize > 0) {
|
||||
// Since the allocateArray can move glyph, make one that stays in one place.
|
||||
SkGlyph stationaryGlyph = *glyph;
|
||||
stationaryGlyph.fImage = serializer.allocateArray<uint8_t>(imageSize);
|
||||
scaler->getImage(stationaryGlyph);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OpCode::kPrepopulateCache : {
|
||||
read_strikes_spec_write_strikes_data(
|
||||
&deserializer, &serializer, this);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
SK_ABORT("Bad op");
|
||||
}
|
||||
}
|
||||
|
||||
void SkStrikeServer::prepareSerializeProcs(SkSerialProcs* procs) {
|
||||
auto encode = [](SkTypeface* tf, void* ctx) {
|
||||
return reinterpret_cast<SkStrikeServer*>(ctx)->encodeTypeface(tf);
|
||||
};
|
||||
procs->fTypefaceProc = encode;
|
||||
procs->fTypefaceCtx = this;
|
||||
}
|
||||
|
||||
SkScalerContext* SkStrikeServer::generateScalerContext(
|
||||
const SkScalerContextRecDescriptor& desc, SkFontID typefaceId)
|
||||
{
|
||||
|
||||
auto scaler = fScalerContextMap.find(desc);
|
||||
if (scaler == nullptr) {
|
||||
auto typefaceIter = fTypefaceMap.find(typefaceId);
|
||||
if (typefaceIter == nullptr) {
|
||||
// TODO: handle this with some future fallback strategy.
|
||||
SK_ABORT("unknown type face");
|
||||
// Should never happen
|
||||
return nullptr;
|
||||
}
|
||||
auto tf = typefaceIter->get();
|
||||
// TODO: make effects really work.
|
||||
SkScalerContextEffects effects;
|
||||
auto mapSc = tf->createScalerContext(effects, &desc.desc(), false);
|
||||
scaler = fScalerContextMap.set(desc, std::move(mapSc));
|
||||
}
|
||||
return scaler->get();
|
||||
}
|
||||
|
||||
sk_sp<SkData> SkStrikeServer::encodeTypeface(SkTypeface* tf) {
|
||||
WireTypeface wire = {
|
||||
SkTypeface::UniqueID(tf),
|
||||
tf->countGlyphs(),
|
||||
tf->fontStyle(),
|
||||
tf->isFixedPitch()
|
||||
};
|
||||
auto typeFace = fTypefaceMap.find(SkTypeface::UniqueID(tf));
|
||||
if (typeFace == nullptr) {
|
||||
fTypefaceMap.set(SkTypeface::UniqueID(tf), sk_ref_sp(tf));
|
||||
}
|
||||
// Can this be done with no copy?
|
||||
return SkData::MakeWithCopy(&wire, sizeof(wire));
|
||||
}
|
||||
|
||||
// -- SkStrikeClient -------------------------------------------------------------------------------
|
||||
SkStrikeClient::SkStrikeClient(SkStrikeCacheClientRPC clientRPC)
|
||||
: fClientRPC{clientRPC} { }
|
||||
|
||||
void SkStrikeClient::generateFontMetrics(
|
||||
const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkPaint::FontMetrics* metrics)
|
||||
{
|
||||
fBuffer.clear();
|
||||
|
||||
Serializer serializer{&fBuffer};
|
||||
serializer.emplace_back<Op>(OpCode::kFontMetrics, typefaceProxy.remoteTypefaceID(), rec);
|
||||
|
||||
auto outBuffer = SkData::MakeWithoutCopy(fBuffer.data(), fBuffer.size());
|
||||
auto inbuffer = fClientRPC(*outBuffer);
|
||||
Deserializer deserializer(*inbuffer);
|
||||
*metrics = *deserializer.read<SkPaint::FontMetrics>();
|
||||
}
|
||||
|
||||
void SkStrikeClient::generateMetricsAndImage(
|
||||
const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkArenaAlloc* alloc,
|
||||
SkGlyph* glyph)
|
||||
{
|
||||
fBuffer.clear();
|
||||
Serializer serializer(&fBuffer);
|
||||
Op *op = serializer.emplace_back<Op>(
|
||||
OpCode::kGlyphMetricsAndImage, typefaceProxy.remoteTypefaceID(), rec);
|
||||
op->glyphID = glyph->getPackedID();
|
||||
|
||||
auto outBuffer = SkData::MakeWithoutCopy(fBuffer.data(), fBuffer.size());
|
||||
auto inbuffer = fClientRPC(*outBuffer);
|
||||
Deserializer deserializer(*inbuffer);
|
||||
*glyph = *deserializer.read<SkGlyph>();
|
||||
auto imageSize = glyph->computeImageSize();
|
||||
glyph->fPathData = nullptr;
|
||||
glyph->fImage = nullptr;
|
||||
if (imageSize > 0) {
|
||||
auto image = deserializer.readArray<uint8_t>(imageSize);
|
||||
SkASSERT(imageSize == image.size());
|
||||
glyph->allocImage(alloc);
|
||||
memcpy(glyph->fImage, image.data(), imageSize);
|
||||
}
|
||||
}
|
||||
|
||||
bool SkStrikeClient::generatePath(
|
||||
const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkGlyphID glyphID,
|
||||
SkPath* path)
|
||||
{
|
||||
fBuffer.clear();
|
||||
|
||||
Serializer serializer{&fBuffer};
|
||||
Op *op = serializer.emplace_back<Op>(
|
||||
OpCode::kGlyphPath, typefaceProxy.remoteTypefaceID(), rec);
|
||||
op->glyphID = glyphID;
|
||||
|
||||
auto outBuffer = SkData::MakeWithoutCopy(fBuffer.data(), fBuffer.size());
|
||||
auto inbuffer = fClientRPC(*outBuffer);
|
||||
Deserializer deserializer(*inbuffer);
|
||||
size_t pathSize = *deserializer.read<size_t>();
|
||||
if (pathSize == 0) {
|
||||
return false;
|
||||
}
|
||||
auto rawPath = deserializer.readArray<uint8_t>(pathSize);
|
||||
path->readFromMemory(rawPath.data(), rawPath.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkStrikeClient::primeStrikeCache(const SkStrikeCacheDifferenceSpec& strikeDifferences) {
|
||||
fBuffer.clear();
|
||||
fBuffer.reserve(strikeDifferences.sizeBytes());
|
||||
|
||||
Serializer serializer{&fBuffer};
|
||||
write_strikes_spec(strikeDifferences, &serializer);
|
||||
|
||||
auto outBuffer = SkData::MakeWithoutCopy(fBuffer.data(), fBuffer.size());
|
||||
auto inbuffer = fClientRPC(*outBuffer);
|
||||
Deserializer deserializer(*inbuffer);
|
||||
update_caches_from_strikes_data(this, &deserializer);
|
||||
}
|
||||
|
||||
void SkStrikeClient::prepareDeserializeProcs(SkDeserialProcs* procs) {
|
||||
auto decode = [](const void* buf, size_t len, void* ctx) {
|
||||
return reinterpret_cast<SkStrikeClient*>(ctx)->decodeTypeface(buf, len);
|
||||
};
|
||||
procs->fTypefaceProc = decode;
|
||||
procs->fTypefaceCtx = this;
|
||||
|
||||
}
|
||||
|
||||
SkTypeface* SkStrikeClient::lookupTypeface(SkFontID id) {
|
||||
auto typeface = fMapIdToTypeface.find(id);
|
||||
SkASSERT(typeface != nullptr);
|
||||
return typeface->get();
|
||||
}
|
||||
|
||||
sk_sp<SkTypeface> SkStrikeClient::decodeTypeface(const void* buf, size_t len) {
|
||||
sk_sp<SkTypeface> SkStrikeClient::deserializeTypeface(const void* buf, size_t len) {
|
||||
WireTypeface wire;
|
||||
if (len < sizeof(wire)) {
|
||||
SK_ABORT("Incomplete transfer");
|
||||
return nullptr;
|
||||
}
|
||||
if (len != sizeof(wire)) return nullptr;
|
||||
|
||||
memcpy(&wire, buf, sizeof(wire));
|
||||
|
||||
auto typeFace = fMapIdToTypeface.find(wire.typefaceID);
|
||||
if (typeFace == nullptr) {
|
||||
auto newTypeface = sk_make_sp<SkTypefaceProxy>(
|
||||
wire.typefaceID,
|
||||
wire.glyphCount,
|
||||
wire.style,
|
||||
wire.isFixed,
|
||||
this);
|
||||
|
||||
typeFace = fMapIdToTypeface.set(wire.typefaceID, newTypeface);
|
||||
}
|
||||
return *typeFace;
|
||||
return addTypeface(wire);
|
||||
}
|
||||
|
||||
sk_sp<SkTypeface> SkStrikeClient::addTypeface(const WireTypeface& wire) {
|
||||
auto* typeface = fRemoteFontIdToTypeface.find(wire.typefaceID);
|
||||
if (typeface) return *typeface;
|
||||
|
||||
auto newTypeface = sk_make_sp<SkTypefaceProxy>(wire.typefaceID, wire.glyphCount, wire.style,
|
||||
wire.isFixed, this);
|
||||
fRemoteFontIdToTypeface.set(wire.typefaceID, newTypeface);
|
||||
return newTypeface;
|
||||
}
|
||||
|
||||
void SkStrikeClient::generateFontMetrics(const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkPaint::FontMetrics* metrics) {
|
||||
TRACE_EVENT1("skia", "generateFontMetrics", "rec", TRACE_STR_COPY(rec.dump().c_str()));
|
||||
SkDebugf("generateFontMetrics: %s\n", rec.dump().c_str());
|
||||
SkStrikeCache::Dump();
|
||||
SkDEBUGFAIL("GlyphCacheMiss");
|
||||
}
|
||||
|
||||
void SkStrikeClient::generateMetricsAndImage(const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkArenaAlloc* alloc,
|
||||
SkGlyph* glyph) {
|
||||
TRACE_EVENT1("skia", "generateMetricsAndImage", "rec", TRACE_STR_COPY(rec.dump().c_str()));
|
||||
SkDebugf("generateMetricsAndImage: %s\n", rec.dump().c_str());
|
||||
SkStrikeCache::Dump();
|
||||
SkDEBUGFAIL("GlyphCacheMiss");
|
||||
}
|
||||
|
||||
void SkStrikeClient::generatePath(const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkGlyphID glyphID,
|
||||
SkPath* path) {
|
||||
TRACE_EVENT1("skia", "generateMetricsAndImage", "rec", TRACE_STR_COPY(rec.dump().c_str()));
|
||||
SkDebugf("generatePath: %s\n", rec.dump().c_str());
|
||||
SkStrikeCache::Dump();
|
||||
SkDEBUGFAIL("GlyphCacheMiss");
|
||||
}
|
||||
|
@ -5,192 +5,196 @@
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#ifndef SkRemoteGlyphCache_DEFINED
|
||||
#define SkRemoteGlyphCache_DEFINED
|
||||
#ifndef SkRemoteGlyphCachePriv_DEFINED
|
||||
#define SkRemoteGlyphCachePriv_DEFINED
|
||||
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "../private/SkTHash.h"
|
||||
#include "SkData.h"
|
||||
#include "SkDescriptor.h"
|
||||
#include "SkDrawLooper.h"
|
||||
#include "SkGlyphCache.h"
|
||||
#include "SkMakeUnique.h"
|
||||
#include "SkNoDrawCanvas.h"
|
||||
#include "SkRefCnt.h"
|
||||
#include "SkRemoteGlyphCache.h"
|
||||
#include "SkSerialProcs.h"
|
||||
#include "SkStrikeCache.h"
|
||||
#include "SkTextBlobRunIterator.h"
|
||||
#include "SkTHash.h"
|
||||
#include "SkTypeface.h"
|
||||
#include "SkTypeface_remote.h"
|
||||
|
||||
// The client uses a SkStrikeCacheClientRPC to send and receive data.
|
||||
using SkStrikeCacheClientRPC = std::function<sk_sp<SkData>(const SkData&)>;
|
||||
class Serializer;
|
||||
class SkDescriptor;
|
||||
class SkGlyphCache;
|
||||
struct SkPackedGlyphID;
|
||||
class SkScalerContextRecDescriptor;
|
||||
class SkTextBlobRunIterator;
|
||||
class SkTypefaceProxy;
|
||||
struct WireTypeface;
|
||||
|
||||
class SkScalerContextRecDescriptor {
|
||||
public:
|
||||
SkScalerContextRecDescriptor() {}
|
||||
explicit SkScalerContextRecDescriptor(const SkScalerContextRec& rec) {
|
||||
auto desc = reinterpret_cast<SkDescriptor*>(&fDescriptor);
|
||||
desc->init();
|
||||
desc->addEntry(kRec_SkDescriptorTag, sizeof(rec), &rec);
|
||||
desc->computeChecksum();
|
||||
SkASSERT(sizeof(fDescriptor) == desc->getLength());
|
||||
}
|
||||
class SkStrikeServer;
|
||||
|
||||
explicit SkScalerContextRecDescriptor(const SkDescriptor& desc)
|
||||
: SkScalerContextRecDescriptor(ExtractRec(desc)) { }
|
||||
|
||||
SkScalerContextRecDescriptor& operator=(const SkScalerContextRecDescriptor& rhs) {
|
||||
std::memcpy(&fDescriptor, &rhs.fDescriptor, rhs.desc().getLength());
|
||||
return *this;
|
||||
}
|
||||
|
||||
const SkDescriptor& desc() const {
|
||||
return *reinterpret_cast<const SkDescriptor*>(&fDescriptor);
|
||||
}
|
||||
|
||||
struct Hash {
|
||||
uint32_t operator()(SkScalerContextRecDescriptor const& s) const {
|
||||
return s.desc().getChecksum();
|
||||
}
|
||||
};
|
||||
|
||||
friend bool operator==(const SkScalerContextRecDescriptor& lhs,
|
||||
const SkScalerContextRecDescriptor& rhs ) {
|
||||
return lhs.desc() == rhs.desc();
|
||||
}
|
||||
|
||||
private:
|
||||
static SkScalerContextRec ExtractRec(const SkDescriptor& desc) {
|
||||
uint32_t size;
|
||||
auto recPtr = desc.findEntry(kRec_SkDescriptorTag, &size);
|
||||
|
||||
SkScalerContextRec result;
|
||||
std::memcpy(&result, recPtr, size);
|
||||
return result;
|
||||
}
|
||||
// The system only passes descriptors without effects. That is why it uses a fixed size
|
||||
// descriptor. storageFor is needed because some of the constructors below are private.
|
||||
template <typename T>
|
||||
using storageFor = typename std::aligned_storage<sizeof(T), alignof(T)>::type;
|
||||
struct {
|
||||
storageFor<SkDescriptor> dummy1;
|
||||
storageFor<SkDescriptor::Entry> dummy2;
|
||||
storageFor<SkScalerContextRec> dummy3;
|
||||
} fDescriptor;
|
||||
struct SkDescriptorMapOperators {
|
||||
size_t operator()(const SkDescriptor* key) const;
|
||||
bool operator()(const SkDescriptor* lhs, const SkDescriptor* rhs) const;
|
||||
};
|
||||
|
||||
class SkStrikeDifferences {
|
||||
public:
|
||||
SkStrikeDifferences(SkFontID typefaceID, std::unique_ptr<SkDescriptor> desc);
|
||||
void add(uint16_t glyphID, SkIPoint pos);
|
||||
SkFontID fTypefaceID;
|
||||
std::unique_ptr<SkDescriptor> fDesc;
|
||||
std::unique_ptr<SkTHashSet<SkPackedGlyphID>> fGlyphIDs =
|
||||
skstd::make_unique<SkTHashSet<SkPackedGlyphID>>();
|
||||
};
|
||||
template <typename T>
|
||||
using SkDescriptorMap = std::unordered_map<const SkDescriptor*, T, SkDescriptorMapOperators,
|
||||
SkDescriptorMapOperators>;
|
||||
|
||||
class SkStrikeCacheDifferenceSpec {
|
||||
public:
|
||||
SkStrikeDifferences& findStrikeDifferences(const SkDescriptor& desc, SkFontID typefaceID);
|
||||
int strikeCount() const { return fDescriptorToDifferencesMap.size(); }
|
||||
size_t sizeBytes() const;
|
||||
template <typename PerStrike, typename PerGlyph>
|
||||
void iterateDifferences(PerStrike perStrike, PerGlyph perGlyph) const;
|
||||
using SkDescriptorSet =
|
||||
std::unordered_set<const SkDescriptor*, SkDescriptorMapOperators, SkDescriptorMapOperators>;
|
||||
|
||||
private:
|
||||
SkDescriptorMap<SkStrikeDifferences> fDescriptorToDifferencesMap{16};
|
||||
};
|
||||
|
||||
class SkTextBlobCacheDiffCanvas : public SkNoDrawCanvas {
|
||||
// A SkTextBlobCacheDiffCanvas is used to populate the SkStrikeServer with ops
|
||||
// which will be serialized and renderered using the SkStrikeClient.
|
||||
class SK_API SkTextBlobCacheDiffCanvas : public SkNoDrawCanvas {
|
||||
public:
|
||||
SkTextBlobCacheDiffCanvas(int width, int height,
|
||||
const SkMatrix& deviceMatrix,
|
||||
const SkSurfaceProps& props,
|
||||
SkScalerContextFlags flags,
|
||||
SkStrikeCacheDifferenceSpec* strikeDiffs);
|
||||
SkTextBlobCacheDiffCanvas(int width, int height, const SkMatrix& deviceMatrix,
|
||||
const SkSurfaceProps& props, SkStrikeServer* strikeserver);
|
||||
~SkTextBlobCacheDiffCanvas() override;
|
||||
|
||||
protected:
|
||||
SaveLayerStrategy getSaveLayerStrategy(const SaveLayerRec& rec) override;
|
||||
SkCanvas::SaveLayerStrategy getSaveLayerStrategy(const SaveLayerRec& rec) override;
|
||||
|
||||
void onDrawTextBlob(
|
||||
const SkTextBlob* blob, SkScalar x, SkScalar y, const SkPaint& paint) override;
|
||||
void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y,
|
||||
const SkPaint& paint) override;
|
||||
|
||||
private:
|
||||
void processLooper(
|
||||
const SkPoint& position,
|
||||
const SkTextBlobRunIterator& it,
|
||||
const SkPaint& origPaint,
|
||||
SkDrawLooper* looper);
|
||||
|
||||
void processGlyphRun(
|
||||
const SkPoint& position,
|
||||
const SkTextBlobRunIterator& it,
|
||||
const SkPaint& runPaint);
|
||||
void processLooper(const SkPoint& position,
|
||||
const SkTextBlobRunIterator& it,
|
||||
const SkPaint& origPaint,
|
||||
SkDrawLooper* looper);
|
||||
void processGlyphRun(const SkPoint& position,
|
||||
const SkTextBlobRunIterator& it,
|
||||
const SkPaint& runPaint);
|
||||
|
||||
const SkMatrix fDeviceMatrix;
|
||||
const SkSurfaceProps fSurfaceProps;
|
||||
const SkScalerContextFlags fScalerContextFlags;
|
||||
|
||||
SkStrikeCacheDifferenceSpec* const fStrikeCacheDiff;
|
||||
SkStrikeServer* const fStrikeServer;
|
||||
};
|
||||
|
||||
class SkStrikeServer {
|
||||
using SkDiscardableHandleId = uint32_t;
|
||||
|
||||
// This class is not thread-safe.
|
||||
class SK_API SkStrikeServer {
|
||||
public:
|
||||
SkStrikeServer();
|
||||
// An interface used by the server to create handles for pinning SkGlyphCache
|
||||
// entries on the remote client.
|
||||
class SK_API DiscardableHandleManager {
|
||||
public:
|
||||
virtual ~DiscardableHandleManager() {}
|
||||
|
||||
// Creates a new *locked* handle and returns a unique ID that can be used to identify
|
||||
// it on the remote client.
|
||||
virtual SkDiscardableHandleId createHandle() = 0;
|
||||
|
||||
// Returns true if the handle could be successfully locked. The server can
|
||||
// assume it will remain locked until the next set of serialized entries is
|
||||
// pulled from the SkStrikeServer.
|
||||
// If returns false, the cache entry mapped to the handle has been deleted
|
||||
// on the client. Any subsequent attempts to lock the same handle are not
|
||||
// allowed.
|
||||
virtual bool lockHandle(SkDiscardableHandleId) = 0;
|
||||
|
||||
// TODO(khushalsagar): Add an API which checks whether a handle is still
|
||||
// valid without locking, so we can avoid tracking stale handles once they
|
||||
// have been purged on the remote side.
|
||||
};
|
||||
|
||||
SkStrikeServer(DiscardableHandleManager* discardableHandleManager);
|
||||
~SkStrikeServer();
|
||||
|
||||
// embedding clients call these methods
|
||||
void serve(const SkData&, std::vector<uint8_t>*);
|
||||
// Serializes the typeface to be remoted using this server.
|
||||
sk_sp<SkData> serializeTypeface(SkTypeface*);
|
||||
|
||||
void prepareSerializeProcs(SkSerialProcs* procs);
|
||||
// Serializes the strike data captured using a SkTextBlobCacheDiffCanvas. Any
|
||||
// handles locked using the DiscardableHandleManager will be assumed to be
|
||||
// unlocked after this call.
|
||||
void writeStrikeData(std::vector<uint8_t>* memory);
|
||||
|
||||
// mostly called internally by Skia
|
||||
SkScalerContext* generateScalerContext(
|
||||
const SkScalerContextRecDescriptor& desc, SkFontID typefaceId);
|
||||
// Methods used internally in skia ------------------------------------------
|
||||
class SkGlyphCacheState {
|
||||
public:
|
||||
SkGlyphCacheState(std::unique_ptr<SkDescriptor> desc,
|
||||
SkDiscardableHandleId discardableHandleId);
|
||||
~SkGlyphCacheState();
|
||||
|
||||
void addGlyph(SkTypeface*, const SkScalerContextEffects&, SkPackedGlyphID);
|
||||
void writePendingGlyphs(Serializer* serializer);
|
||||
bool has_pending_glyphs() const { return !fPendingGlyphs.empty(); }
|
||||
SkDiscardableHandleId discardable_handle_id() const { return fDiscardableHandleId; }
|
||||
|
||||
private:
|
||||
// The set of glyphs cached on the remote client.
|
||||
SkTHashSet<SkPackedGlyphID> fCachedGlyphs;
|
||||
|
||||
// The set of glyphs which has not yet been serialized and sent to the
|
||||
// remote client.
|
||||
std::vector<SkPackedGlyphID> fPendingGlyphs;
|
||||
|
||||
std::unique_ptr<SkDescriptor> fDesc;
|
||||
const SkDiscardableHandleId fDiscardableHandleId = -1;
|
||||
std::unique_ptr<SkScalerContext> fContext;
|
||||
};
|
||||
SkGlyphCacheState* getOrCreateCache(SkTypeface*, std::unique_ptr<SkDescriptor>);
|
||||
|
||||
private:
|
||||
using DescriptorToContextMap = SkTHashMap<SkScalerContextRecDescriptor,
|
||||
std::unique_ptr<SkScalerContext>,
|
||||
SkScalerContextRecDescriptor::Hash>;
|
||||
SkDescriptorMap<std::unique_ptr<SkGlyphCacheState>> fRemoteGlyphStateMap;
|
||||
DiscardableHandleManager* const fDiscardableHandleManager;
|
||||
SkTHashSet<SkFontID> fCachedTypefaces;
|
||||
|
||||
sk_sp<SkData> encodeTypeface(SkTypeface* tf);
|
||||
|
||||
int fOpCount = 0;
|
||||
SkTHashMap<SkFontID, sk_sp<SkTypeface>> fTypefaceMap;
|
||||
DescriptorToContextMap fScalerContextMap;
|
||||
// State cached until the next serialization.
|
||||
SkDescriptorSet fLockedDescs;
|
||||
std::vector<WireTypeface> fTypefacesToSend;
|
||||
};
|
||||
|
||||
class SkStrikeClient {
|
||||
class SK_API SkStrikeClient {
|
||||
public:
|
||||
SkStrikeClient(SkStrikeCacheClientRPC);
|
||||
// An interface to delete handles that may be pinned by the remote server.
|
||||
class DiscardableHandleManager : public SkRefCnt {
|
||||
public:
|
||||
virtual ~DiscardableHandleManager() {}
|
||||
|
||||
// embedding clients call these methods
|
||||
void primeStrikeCache(const SkStrikeCacheDifferenceSpec&);
|
||||
void prepareDeserializeProcs(SkDeserialProcs* procs);
|
||||
// Returns true if the handle was unlocked and can be safely deleted. Once
|
||||
// successful, subsequent attempts to delete the same handle are invalid.
|
||||
virtual bool deleteHandle(SkDiscardableHandleId) = 0;
|
||||
};
|
||||
|
||||
// mostly called internally by Skia
|
||||
void generateFontMetrics(
|
||||
const SkTypefaceProxy&, const SkScalerContextRec&, SkPaint::FontMetrics*);
|
||||
void generateMetricsAndImage(
|
||||
const SkTypefaceProxy&, const SkScalerContextRec&, SkArenaAlloc*, SkGlyph*);
|
||||
bool generatePath(
|
||||
const SkTypefaceProxy&, const SkScalerContextRec&, SkGlyphID glyph, SkPath* path);
|
||||
SkTypeface* lookupTypeface(SkFontID id);
|
||||
SkStrikeClient(sk_sp<DiscardableHandleManager>);
|
||||
~SkStrikeClient();
|
||||
|
||||
// Deserializes the typeface previously serialized using the SkStrikeServer. Returns null if the
|
||||
// data is invalid.
|
||||
sk_sp<SkTypeface> deserializeTypeface(const void* data, size_t length);
|
||||
|
||||
// Deserializes the strike data from a SkStrikeServer. All messages generated
|
||||
// from a server when serializing the ops must be deserialized before the op
|
||||
// is rasterized.
|
||||
// Returns false if the data is invalid.
|
||||
bool readStrikeData(const volatile void* memory, size_t memorySize);
|
||||
|
||||
// TODO: Remove these since we don't support pulling this data on-demand.
|
||||
void generateFontMetrics(const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkPaint::FontMetrics* metrics);
|
||||
void generateMetricsAndImage(const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkArenaAlloc* alloc,
|
||||
SkGlyph* glyph);
|
||||
void generatePath(const SkTypefaceProxy& typefaceProxy,
|
||||
const SkScalerContextRec& rec,
|
||||
SkGlyphID glyphID,
|
||||
SkPath* path);
|
||||
|
||||
private:
|
||||
sk_sp<SkTypeface> decodeTypeface(const void* buf, size_t len);
|
||||
class DiscardableStrikePinner;
|
||||
|
||||
// TODO: Figure out how to manage the entries for the following maps.
|
||||
SkTHashMap<SkFontID, sk_sp<SkTypefaceProxy>> fMapIdToTypeface;
|
||||
sk_sp<SkTypeface> addTypeface(const WireTypeface& wire);
|
||||
|
||||
SkStrikeCacheClientRPC fClientRPC;
|
||||
|
||||
std::vector<uint8_t> fBuffer;
|
||||
SkTHashMap<SkFontID, sk_sp<SkTypeface>> fRemoteFontIdToTypeface;
|
||||
sk_sp<DiscardableHandleManager> fDiscardableHandleManager;
|
||||
};
|
||||
|
||||
#endif // SkRemoteGlyphCache_DEFINED
|
||||
#endif // SkRemoteGlyphCachePriv_DEFINED
|
||||
|
@ -135,8 +135,8 @@ public:
|
||||
fPost2x2[0][1], fPost2x2[1][0], fPost2x2[1][1]);
|
||||
msg.appendf(" frame %g miter %g format %d join %d cap %d flags %#hx\n",
|
||||
fFrameWidth, fMiterLimit, fMaskFormat, fStrokeJoin, fStrokeCap, fFlags);
|
||||
msg.appendf(" lum bits %x, device gamma %d, paint gamma %d contrast %d\n",
|
||||
fLumBits, fDeviceGamma, fPaintGamma, fContrast);
|
||||
msg.appendf(" lum bits %x, device gamma %d, paint gamma %d contrast %d\n", fLumBits,
|
||||
fDeviceGamma, fPaintGamma, fContrast);
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
#define SkStrikeCache_DEFINED
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "SkDescriptor.h"
|
||||
#include "SkSpinlock.h"
|
||||
@ -31,24 +32,6 @@ class SkTraceMemoryDump;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
struct SkDescriptorMapOperators {
|
||||
size_t operator()(const SkDescriptor* key) const {
|
||||
return key->getChecksum();
|
||||
}
|
||||
|
||||
bool operator()(const SkDescriptor* lhs, const SkDescriptor* rhs) const {
|
||||
return *lhs == *rhs;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using SkDescriptorMap =
|
||||
std::unordered_map<
|
||||
const SkDescriptor*,
|
||||
T,
|
||||
SkDescriptorMapOperators,
|
||||
SkDescriptorMapOperators>;
|
||||
|
||||
class SkStrikePinner {
|
||||
public:
|
||||
virtual ~SkStrikePinner() = default;
|
||||
|
@ -6,17 +6,15 @@
|
||||
*/
|
||||
|
||||
#include "SkTypeface_remote.h"
|
||||
|
||||
#include "SkPaint.h"
|
||||
#include "SkRemoteGlyphCache.h"
|
||||
|
||||
SkScalerContextProxy::SkScalerContextProxy(
|
||||
sk_sp<SkTypeface> tf,
|
||||
const SkScalerContextEffects& effects,
|
||||
const SkDescriptor* desc,
|
||||
SkStrikeClient* rsc)
|
||||
: SkScalerContext{std::move(tf), effects, desc}
|
||||
, fClient{rsc} {}
|
||||
#include "SkPaint.h"
|
||||
|
||||
SkScalerContextProxy::SkScalerContextProxy(sk_sp<SkTypeface> tf,
|
||||
const SkScalerContextEffects& effects,
|
||||
const SkDescriptor* desc,
|
||||
SkStrikeClient* rsc)
|
||||
: SkScalerContext{std::move(tf), effects, desc}, fClient{rsc} {}
|
||||
|
||||
unsigned SkScalerContextProxy::generateGlyphCount() {
|
||||
SK_ABORT("Should never be called.");
|
||||
@ -40,7 +38,8 @@ void SkScalerContextProxy::generateImage(const SkGlyph& glyph) {
|
||||
}
|
||||
|
||||
bool SkScalerContextProxy::generatePath(SkGlyphID glyphID, SkPath* path) {
|
||||
return fClient->generatePath(*this->typefaceProxy(), this->getRec(), glyphID, path);
|
||||
fClient->generatePath(*this->typefaceProxy(), this->getRec(), glyphID, path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkScalerContextProxy::generateFontMetrics(SkPaint::FontMetrics* metrics) {
|
||||
|
@ -21,11 +21,10 @@ class SkTypefaceProxy;
|
||||
|
||||
class SkScalerContextProxy : public SkScalerContext {
|
||||
public:
|
||||
SkScalerContextProxy(
|
||||
sk_sp<SkTypeface> tf,
|
||||
const SkScalerContextEffects& effects,
|
||||
const SkDescriptor* desc,
|
||||
SkStrikeClient* rsc);
|
||||
SkScalerContextProxy(sk_sp<SkTypeface> tf,
|
||||
const SkScalerContextEffects& effects,
|
||||
const SkDescriptor* desc,
|
||||
SkStrikeClient* rsc);
|
||||
|
||||
protected:
|
||||
unsigned generateGlyphCount() override;
|
||||
@ -52,16 +51,12 @@ private:
|
||||
|
||||
class SkTypefaceProxy : public SkTypeface {
|
||||
public:
|
||||
SkTypefaceProxy(
|
||||
SkFontID fontId,
|
||||
int glyphCount,
|
||||
const SkFontStyle& style,
|
||||
bool isFixed,
|
||||
SkStrikeClient* rsc)
|
||||
: INHERITED{style, false}
|
||||
, fFontId{fontId}
|
||||
, fGlyphCount{glyphCount}
|
||||
, fRsc{rsc} { }
|
||||
SkTypefaceProxy(SkFontID fontId,
|
||||
int glyphCount,
|
||||
const SkFontStyle& style,
|
||||
bool isFixed,
|
||||
SkStrikeClient* rsc)
|
||||
: INHERITED{style, false}, fFontId{fontId}, fGlyphCount{glyphCount}, fRsc{rsc} {}
|
||||
SkFontID remoteTypefaceID() const {return fFontId;}
|
||||
int glyphCount() const {return fGlyphCount;}
|
||||
static SkTypefaceProxy* DownCast(SkTypeface* typeface) {
|
||||
@ -138,6 +133,8 @@ protected:
|
||||
private:
|
||||
const SkFontID fFontId;
|
||||
const int fGlyphCount;
|
||||
|
||||
// TODO: Does this need a ref to the strike client? If yes, make it a weak ref.
|
||||
SkStrikeClient* const fRsc;
|
||||
|
||||
typedef SkTypeface INHERITED;
|
||||
|
220
tests/SkRemoteGlyphCacheTest.cpp
Normal file
220
tests/SkRemoteGlyphCacheTest.cpp
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2018 Google Inc.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license that can be
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "SkGraphics.h"
|
||||
#include "SkMutex.h"
|
||||
#include "SkRemoteGlyphCache.h"
|
||||
#include "SkStrikeCache.h"
|
||||
#include "SkSurface.h"
|
||||
#include "SkTextBlob.h"
|
||||
#include "SkTypeface_remote.h"
|
||||
#include "Test.h"
|
||||
|
||||
class DiscardableManager : public SkStrikeServer::DiscardableHandleManager,
|
||||
public SkStrikeClient::DiscardableHandleManager {
|
||||
public:
|
||||
DiscardableManager() = default;
|
||||
~DiscardableManager() override = default;
|
||||
|
||||
// Server implementation.
|
||||
SkDiscardableHandleId createHandle() override {
|
||||
// Handles starts as locked.
|
||||
fLockedHandles.add(++fNextHandleId);
|
||||
return fNextHandleId;
|
||||
}
|
||||
bool lockHandle(SkDiscardableHandleId id) override {
|
||||
if (id <= fLastDeletedHandleId) return false;
|
||||
fLockedHandles.add(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Client implementation.
|
||||
bool deleteHandle(SkDiscardableHandleId id) override { return id <= fLastDeletedHandleId; }
|
||||
|
||||
void unlockAll() { fLockedHandles.reset(); }
|
||||
void unlockAndDeleteAll() {
|
||||
unlockAll();
|
||||
fLastDeletedHandleId = fNextHandleId;
|
||||
}
|
||||
const SkTHashSet<SkDiscardableHandleId>& lockedHandles() const { return fLockedHandles; }
|
||||
SkDiscardableHandleId handleCount() { return fNextHandleId; }
|
||||
|
||||
private:
|
||||
SkDiscardableHandleId fNextHandleId = 0u;
|
||||
SkDiscardableHandleId fLastDeletedHandleId = 0u;
|
||||
SkTHashSet<SkDiscardableHandleId> fLockedHandles;
|
||||
};
|
||||
|
||||
sk_sp<SkTextBlob> buildTextBlob(sk_sp<SkTypeface> tf, int glyphCount) {
|
||||
SkPaint font;
|
||||
font.setTypeface(tf);
|
||||
font.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
|
||||
font.setTextAlign(SkPaint::kLeft_Align);
|
||||
font.setStyle(SkPaint::kFill_Style);
|
||||
font.setHinting(SkPaint::kNormal_Hinting);
|
||||
font.setTextSize(1u);
|
||||
|
||||
SkTextBlobBuilder builder;
|
||||
SkRect bounds = SkRect::MakeWH(10, 10);
|
||||
const auto& runBuffer = builder.allocRunPosH(font, glyphCount, 0, &bounds);
|
||||
for (int i = 0; i < glyphCount; i++) runBuffer.glyphs[i] = static_cast<SkGlyphID>(i);
|
||||
return builder.make();
|
||||
}
|
||||
|
||||
SkBitmap RasterBlob(sk_sp<SkTextBlob> blob, int width, int height) {
|
||||
auto surface = SkSurface::MakeRasterN32Premul(width, height);
|
||||
SkPaint paint;
|
||||
surface->getCanvas()->drawTextBlob(blob.get(), 0u, 0u, paint);
|
||||
SkBitmap bitmap;
|
||||
bitmap.allocN32Pixels(width, height);
|
||||
surface->readPixels(bitmap, 0, 0);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
DEF_TEST(SkRemoteGlyphCache_TypefaceSerialization, reporter) {
|
||||
sk_sp<DiscardableManager> discardableManager = sk_make_sp<DiscardableManager>();
|
||||
SkStrikeServer server(discardableManager.get());
|
||||
SkStrikeClient client(discardableManager);
|
||||
|
||||
auto server_tf = SkTypeface::MakeDefault();
|
||||
auto tf_data = server.serializeTypeface(server_tf.get());
|
||||
|
||||
auto client_tf = client.deserializeTypeface(tf_data->data(), tf_data->size());
|
||||
REPORTER_ASSERT(reporter, client_tf);
|
||||
REPORTER_ASSERT(reporter, SkTypefaceProxy::DownCast(client_tf.get())->remoteTypefaceID() ==
|
||||
server_tf->uniqueID());
|
||||
}
|
||||
|
||||
#if 0
|
||||
TODO(khushalsagar): Re-enable once crbug.com/831354 is fixed.
|
||||
DEF_TEST(SkRemoteGlyphCache_StrikeSerialization, reporter) {
|
||||
sk_sp<DiscardableManager> discardableManager = sk_make_sp<DiscardableManager>();
|
||||
SkStrikeServer server(discardableManager.get());
|
||||
SkStrikeClient client(discardableManager);
|
||||
|
||||
// Server.
|
||||
auto serverTf = SkTypeface::MakeFromName("monospace", SkFontStyle());
|
||||
auto serverTfData = server.serializeTypeface(serverTf.get());
|
||||
|
||||
int glyphCount = 10;
|
||||
auto serverBlob = buildTextBlob(serverTf, glyphCount);
|
||||
const SkSurfaceProps props(SkSurfaceProps::kLegacyFontHost_InitType);
|
||||
SkTextBlobCacheDiffCanvas cache_diff_canvas(10, 10, SkMatrix::I(), props, &server);
|
||||
SkPaint paint;
|
||||
cache_diff_canvas.drawTextBlob(serverBlob.get(), 0, 0, paint);
|
||||
|
||||
std::vector<uint8_t> serverStrikeData;
|
||||
server.writeStrikeData(&serverStrikeData);
|
||||
|
||||
// Client.
|
||||
auto clientTf = client.deserializeTypeface(serverTfData->data(), serverTfData->size());
|
||||
REPORTER_ASSERT(reporter,
|
||||
client.readStrikeData(serverStrikeData.data(), serverStrikeData.size()));
|
||||
auto clientBlob = buildTextBlob(clientTf, glyphCount);
|
||||
|
||||
SkBitmap expected = RasterBlob(serverBlob, 10, 10);
|
||||
SkBitmap actual = RasterBlob(clientBlob, 10, 10);
|
||||
for (int i = 0; i < expected.width(); ++i) {
|
||||
for (int j = 0; j < expected.height(); ++j) {
|
||||
REPORTER_ASSERT(reporter, expected.getColor(i, j) == actual.getColor(i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
DEF_TEST(SkRemoteGlyphCache_StrikeLockingServer, reporter) {
|
||||
sk_sp<DiscardableManager> discardableManager = sk_make_sp<DiscardableManager>();
|
||||
SkStrikeServer server(discardableManager.get());
|
||||
SkStrikeClient client(discardableManager);
|
||||
|
||||
auto serverTf = SkTypeface::MakeFromName("monospace", SkFontStyle());
|
||||
server.serializeTypeface(serverTf.get());
|
||||
int glyphCount = 10;
|
||||
auto serverBlob = buildTextBlob(serverTf, glyphCount);
|
||||
|
||||
const SkSurfaceProps props(SkSurfaceProps::kLegacyFontHost_InitType);
|
||||
SkTextBlobCacheDiffCanvas cache_diff_canvas(10, 10, SkMatrix::I(), props, &server);
|
||||
SkPaint paint;
|
||||
cache_diff_canvas.drawTextBlob(serverBlob.get(), 0, 0, paint);
|
||||
|
||||
// The strike from the blob should be locked after it has been drawn on the canvas.
|
||||
REPORTER_ASSERT(reporter, discardableManager->handleCount() == 1u);
|
||||
REPORTER_ASSERT(reporter, discardableManager->lockedHandles().count() == 1u);
|
||||
|
||||
// Write the strike data and unlock everything. Re-analyzing the blob should lock the handle
|
||||
// again.
|
||||
std::vector<uint8_t> fontData;
|
||||
server.writeStrikeData(&fontData);
|
||||
discardableManager->unlockAll();
|
||||
REPORTER_ASSERT(reporter, discardableManager->lockedHandles().count() == 0u);
|
||||
|
||||
cache_diff_canvas.drawTextBlob(serverBlob.get(), 0, 0, paint);
|
||||
REPORTER_ASSERT(reporter, discardableManager->handleCount() == 1u);
|
||||
REPORTER_ASSERT(reporter, discardableManager->lockedHandles().count() == 1u);
|
||||
}
|
||||
|
||||
DEF_TEST(SkRemoteGlyphCache_StrikeDeletionServer, reporter) {
|
||||
sk_sp<DiscardableManager> discardableManager = sk_make_sp<DiscardableManager>();
|
||||
SkStrikeServer server(discardableManager.get());
|
||||
SkStrikeClient client(discardableManager);
|
||||
|
||||
auto serverTf = SkTypeface::MakeFromName("monospace", SkFontStyle());
|
||||
server.serializeTypeface(serverTf.get());
|
||||
int glyphCount = 10;
|
||||
auto serverBlob = buildTextBlob(serverTf, glyphCount);
|
||||
|
||||
const SkSurfaceProps props(SkSurfaceProps::kLegacyFontHost_InitType);
|
||||
SkTextBlobCacheDiffCanvas cache_diff_canvas(10, 10, SkMatrix::I(), props, &server);
|
||||
SkPaint paint;
|
||||
cache_diff_canvas.drawTextBlob(serverBlob.get(), 0, 0, paint);
|
||||
REPORTER_ASSERT(reporter, discardableManager->handleCount() == 1u);
|
||||
|
||||
// Write the strike data and delete all the handles. Re-analyzing the blob should create new
|
||||
// handles.
|
||||
std::vector<uint8_t> fontData;
|
||||
server.writeStrikeData(&fontData);
|
||||
discardableManager->unlockAndDeleteAll();
|
||||
cache_diff_canvas.drawTextBlob(serverBlob.get(), 0, 0, paint);
|
||||
printf("HandleCount: %d\n ", discardableManager->handleCount());
|
||||
REPORTER_ASSERT(reporter, discardableManager->handleCount() == 2u);
|
||||
}
|
||||
|
||||
DEF_TEST(SkRemoteGlyphCache_StrikePinningClient, reporter) {
|
||||
sk_sp<DiscardableManager> discardableManager = sk_make_sp<DiscardableManager>();
|
||||
SkStrikeServer server(discardableManager.get());
|
||||
SkStrikeClient client(discardableManager);
|
||||
|
||||
// Server.
|
||||
auto serverTf = SkTypeface::MakeFromName("monospace", SkFontStyle());
|
||||
auto serverTfData = server.serializeTypeface(serverTf.get());
|
||||
|
||||
int glyphCount = 10;
|
||||
auto serverBlob = buildTextBlob(serverTf, glyphCount);
|
||||
|
||||
const SkSurfaceProps props(SkSurfaceProps::kLegacyFontHost_InitType);
|
||||
SkTextBlobCacheDiffCanvas cache_diff_canvas(10, 10, SkMatrix::I(), props, &server);
|
||||
SkPaint paint;
|
||||
cache_diff_canvas.drawTextBlob(serverBlob.get(), 0, 0, paint);
|
||||
|
||||
std::vector<uint8_t> serverStrikeData;
|
||||
server.writeStrikeData(&serverStrikeData);
|
||||
|
||||
// Client.
|
||||
REPORTER_ASSERT(reporter,
|
||||
client.readStrikeData(serverStrikeData.data(), serverStrikeData.size()));
|
||||
auto* clientTf = client.deserializeTypeface(serverTfData->data(), serverTfData->size()).get();
|
||||
|
||||
// The cache remains alive until it is pinned in the discardable manager.
|
||||
SkGraphics::PurgeFontCache();
|
||||
REPORTER_ASSERT(reporter, !clientTf->unique());
|
||||
|
||||
// Once the strike is unpinned and purged, SkStrikeClient should be the only owner of the
|
||||
// clientTf.
|
||||
discardableManager->unlockAndDeleteAll();
|
||||
SkGraphics::PurgeFontCache();
|
||||
REPORTER_ASSERT(reporter, clientTf->unique());
|
||||
}
|
@ -16,8 +16,9 @@
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "SkRemoteGlyphCache.h"
|
||||
#include "SkGraphics.h"
|
||||
#include "SkRemoteGlyphCache.h"
|
||||
#include "SkScalerContext.h"
|
||||
#include "SkSurface.h"
|
||||
|
||||
static std::string gSkpName;
|
||||
@ -25,6 +26,46 @@ static bool gUseGpu = true;
|
||||
static bool gPurgeFontCaches = true;
|
||||
static bool gUseProcess = true;
|
||||
|
||||
class ServerDiscardableManager : public SkStrikeServer::DiscardableHandleManager {
|
||||
public:
|
||||
ServerDiscardableManager() = default;
|
||||
~ServerDiscardableManager() override = default;
|
||||
|
||||
SkDiscardableHandleId createHandle() override { return ++nextHandleId; }
|
||||
bool lockHandle(SkDiscardableHandleId handleId) override {
|
||||
return handleId > lastPurgedHandleId;
|
||||
}
|
||||
void purgeAll() { lastPurgedHandleId = nextHandleId; }
|
||||
|
||||
private:
|
||||
SkDiscardableHandleId nextHandleId = 0u;
|
||||
SkDiscardableHandleId lastPurgedHandleId = 0u;
|
||||
};
|
||||
|
||||
class ClientDiscardableManager : public SkStrikeClient::DiscardableHandleManager {
|
||||
public:
|
||||
class ScopedPurgeCache {
|
||||
public:
|
||||
ScopedPurgeCache(ClientDiscardableManager* manager) : fManager(manager) {
|
||||
if (fManager) fManager->allowPurging = true;
|
||||
}
|
||||
~ScopedPurgeCache() {
|
||||
if (fManager) fManager->allowPurging = false;
|
||||
}
|
||||
|
||||
private:
|
||||
ClientDiscardableManager* fManager;
|
||||
};
|
||||
|
||||
ClientDiscardableManager() = default;
|
||||
~ClientDiscardableManager() override = default;
|
||||
|
||||
bool deleteHandle(SkDiscardableHandleId) override { return allowPurging; }
|
||||
|
||||
private:
|
||||
bool allowPurging = false;
|
||||
};
|
||||
|
||||
static bool write_SkData(int fd, const SkData& data) {
|
||||
size_t size = data.size();
|
||||
ssize_t bytesWritten = ::write(fd, &size, sizeof(size));
|
||||
@ -43,7 +84,6 @@ static bool write_SkData(int fd, const SkData& data) {
|
||||
}
|
||||
|
||||
static sk_sp<SkData> read_SkData(int fd) {
|
||||
|
||||
size_t size;
|
||||
ssize_t readSize = ::read(fd, &size, sizeof(size));
|
||||
if (readSize <= 0) {
|
||||
@ -92,44 +132,56 @@ private:
|
||||
std::chrono::duration<double> fElapsedSeconds{0.0};
|
||||
};
|
||||
|
||||
static void build_prime_cache_spec(const SkIRect &bounds,
|
||||
const SkSurfaceProps &props,
|
||||
const SkPicture &pic,
|
||||
SkStrikeCacheDifferenceSpec *strikeDifference) {
|
||||
static bool push_font_data(const SkPicture& pic, SkStrikeServer* strikeServer, int writeFd) {
|
||||
SkMatrix deviceMatrix = SkMatrix::I();
|
||||
|
||||
SkTextBlobCacheDiffCanvas filter(
|
||||
bounds.width(), bounds.height(), deviceMatrix, props,
|
||||
SkScalerContextFlags::kFakeGammaAndBoostContrast,
|
||||
strikeDifference);
|
||||
|
||||
const SkIRect bounds = pic.cullRect().round();
|
||||
const SkSurfaceProps props(SkSurfaceProps::kLegacyFontHost_InitType);
|
||||
SkTextBlobCacheDiffCanvas filter(bounds.width(), bounds.height(), deviceMatrix, props,
|
||||
strikeServer);
|
||||
pic.playback(&filter);
|
||||
|
||||
std::vector<uint8_t> fontData;
|
||||
strikeServer->writeStrikeData(&fontData);
|
||||
auto data = SkData::MakeWithoutCopy(fontData.data(), fontData.size());
|
||||
return write_SkData(writeFd, *data);
|
||||
}
|
||||
|
||||
static void final_draw(std::string outFilename,
|
||||
SkDeserialProcs* procs,
|
||||
SkData* picData,
|
||||
SkStrikeClient* client) {
|
||||
static void final_draw(std::string outFilename, SkData* picData, SkStrikeClient* client,
|
||||
ClientDiscardableManager* discardableManager, int readFd, int writeFd) {
|
||||
SkDeserialProcs procs;
|
||||
auto decode = [](const void* data, size_t length, void* ctx) -> sk_sp<SkTypeface> {
|
||||
return reinterpret_cast<SkStrikeClient*>(ctx)->deserializeTypeface(data, length);
|
||||
};
|
||||
procs.fTypefaceProc = decode;
|
||||
procs.fTypefaceCtx = client;
|
||||
|
||||
auto pic = SkPicture::MakeFromData(picData, procs);
|
||||
auto pic = SkPicture::MakeFromData(picData, &procs);
|
||||
|
||||
auto cullRect = pic->cullRect();
|
||||
auto r = cullRect.round();
|
||||
|
||||
auto s = SkSurface::MakeRasterN32Premul(r.width(), r.height());
|
||||
auto c = s->getCanvas();
|
||||
auto picUnderTest = SkPicture::MakeFromData(picData, procs);
|
||||
auto picUnderTest = SkPicture::MakeFromData(picData, &procs);
|
||||
|
||||
Timer drawTime;
|
||||
auto randomData = SkData::MakeUninitialized(1u);
|
||||
for (int i = 0; i < 100; i++) {
|
||||
if (gPurgeFontCaches) {
|
||||
ClientDiscardableManager::ScopedPurgeCache purge(discardableManager);
|
||||
SkGraphics::PurgeFontCache();
|
||||
SkASSERT(SkGraphics::GetFontCacheUsed() == 0u);
|
||||
}
|
||||
|
||||
drawTime.start();
|
||||
if (client != nullptr) {
|
||||
SkStrikeCacheDifferenceSpec strikeDifference;
|
||||
build_prime_cache_spec(r, s->props(), *picUnderTest, &strikeDifference);
|
||||
client->primeStrikeCache(strikeDifference);
|
||||
// Kick the renderer to send us the fonts.
|
||||
write_SkData(writeFd, *randomData);
|
||||
auto fontData = read_SkData(readFd);
|
||||
if (fontData && !fontData->isEmpty()) {
|
||||
if (!client->readStrikeData(fontData->data(), fontData->size()))
|
||||
SK_ABORT("Bad serialization");
|
||||
}
|
||||
}
|
||||
c->drawPicture(picUnderTest);
|
||||
drawTime.stop();
|
||||
@ -150,22 +202,16 @@ static void final_draw(std::string outFilename,
|
||||
static void gpu(int readFd, int writeFd) {
|
||||
|
||||
if (gUseGpu) {
|
||||
auto clientRPC = [readFd, writeFd](const SkData& inBuffer) {
|
||||
write_SkData(writeFd, inBuffer);
|
||||
return read_SkData(readFd);
|
||||
};
|
||||
|
||||
auto picData = read_SkData(readFd);
|
||||
if (picData == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
SkStrikeClient client{clientRPC};
|
||||
sk_sp<ClientDiscardableManager> discardableManager = sk_make_sp<ClientDiscardableManager>();
|
||||
SkStrikeClient strikeClient(discardableManager);
|
||||
|
||||
SkDeserialProcs procs;
|
||||
client.prepareDeserializeProcs(&procs);
|
||||
|
||||
final_draw("test.png", &procs, picData.get(), &client);
|
||||
final_draw("test.png", picData.get(), &strikeClient, discardableManager.get(), readFd,
|
||||
writeFd);
|
||||
}
|
||||
|
||||
::close(writeFd);
|
||||
@ -177,7 +223,8 @@ static void gpu(int readFd, int writeFd) {
|
||||
static int renderer(
|
||||
const std::string& skpName, int readFd, int writeFd)
|
||||
{
|
||||
SkStrikeServer server{};
|
||||
ServerDiscardableManager discardableManager;
|
||||
SkStrikeServer server(&discardableManager);
|
||||
auto closeAll = [readFd, writeFd]() {
|
||||
::close(writeFd);
|
||||
::close(readFd);
|
||||
@ -186,11 +233,16 @@ static int renderer(
|
||||
auto skpData = SkData::MakeFromFileName(skpName.c_str());
|
||||
std::cout << "skp stream is " << skpData->size() << " bytes long " << std::endl;
|
||||
|
||||
SkSerialProcs procs;
|
||||
sk_sp<SkData> stream;
|
||||
if (gUseGpu) {
|
||||
auto pic = SkPicture::MakeFromData(skpData.get());
|
||||
server.prepareSerializeProcs(&procs);
|
||||
SkSerialProcs procs;
|
||||
auto encode = [](SkTypeface* tf, void* ctx) -> sk_sp<SkData> {
|
||||
return reinterpret_cast<SkStrikeServer*>(ctx)->serializeTypeface(tf);
|
||||
};
|
||||
procs.fTypefaceProc = encode;
|
||||
procs.fTypefaceCtx = &server;
|
||||
|
||||
stream = pic->serialize(&procs);
|
||||
|
||||
if (!write_SkData(writeFd, *stream)) {
|
||||
@ -198,22 +250,18 @@ static int renderer(
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> tmpBuffer;
|
||||
while (true) {
|
||||
auto inBuffer = read_SkData(readFd);
|
||||
if (inBuffer == nullptr) {
|
||||
closeAll();
|
||||
return 0;
|
||||
}
|
||||
|
||||
tmpBuffer.clear();
|
||||
server.serve(*inBuffer, &tmpBuffer);
|
||||
auto outBuffer = SkData::MakeWithoutCopy(tmpBuffer.data(), tmpBuffer.size());
|
||||
write_SkData(writeFd, *outBuffer);
|
||||
if (gPurgeFontCaches) discardableManager.purgeAll();
|
||||
push_font_data(*pic.get(), &server, writeFd);
|
||||
}
|
||||
} else {
|
||||
stream = skpData;
|
||||
final_draw("test-correct.png", nullptr, stream.get(), nullptr);
|
||||
final_draw("test-correct.png", stream.get(), nullptr, nullptr, -1, -1);
|
||||
closeAll();
|
||||
return 0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user