summaryrefslogtreecommitdiffstats
path: root/common/rfb/EncodeManager.cxx
diff options
context:
space:
mode:
authorPierre Ossman <ossman@cendio.se>2014-03-14 15:59:46 +0100
committerPierre Ossman <ossman@cendio.se>2014-07-14 16:03:42 +0200
commitc0397269fcab67e9acd4fdcbc29f24d79ed0ef39 (patch)
tree41ac251e5a595a37b832a61626723a89258bb018 /common/rfb/EncodeManager.cxx
parenta088f1ab3923482998174b9db8949cf06d0761af (diff)
downloadtigervnc-c0397269fcab67e9acd4fdcbc29f24d79ed0ef39.tar.gz
tigervnc-c0397269fcab67e9acd4fdcbc29f24d79ed0ef39.zip
Move image encoding logic into a central EncodeManager class
This allows us to apply a lot more server logic independently of which encoder is in use. Most of this class are things moved over from the Tight encoder.
Diffstat (limited to 'common/rfb/EncodeManager.cxx')
-rw-r--r--common/rfb/EncodeManager.cxx707
1 files changed, 707 insertions, 0 deletions
diff --git a/common/rfb/EncodeManager.cxx b/common/rfb/EncodeManager.cxx
new file mode 100644
index 00000000..1bd00c7f
--- /dev/null
+++ b/common/rfb/EncodeManager.cxx
@@ -0,0 +1,707 @@
+/* Copyright (C) 2000-2003 Constantin Kaplinsky. All Rights Reserved.
+ * Copyright (C) 2011 D. R. Commander. All Rights Reserved.
+ * Copyright 2014 Pierre Ossman for Cendio AB
+ *
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this software; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+#include <rfb/EncodeManager.h>
+#include <rfb/Encoder.h>
+#include <rfb/Palette.h>
+#include <rfb/SConnection.h>
+#include <rfb/SMsgWriter.h>
+#include <rfb/UpdateTracker.h>
+
+#include <rfb/RawEncoder.h>
+#include <rfb/RREEncoder.h>
+#include <rfb/HextileEncoder.h>
+#include <rfb/ZRLEEncoder.h>
+#include <rfb/TightEncoder.h>
+#include <rfb/TightJPEGEncoder.h>
+
+using namespace rfb;
+
+// Split each rectangle into smaller ones no larger than this area,
+// and no wider than this width.
+static const int SubRectMaxArea = 65536;
+static const int SubRectMaxWidth = 2048;
+
+// The size in pixels of either side of each block tested when looking
+// for solid blocks.
+static const int SolidSearchBlock = 16;
+// Don't bother with blocks smaller than this
+static const int SolidBlockMinArea = 2048;
+
+namespace rfb {
+
+enum EncoderClass {
+ encoderRaw,
+ encoderRRE,
+ encoderHextile,
+ encoderTight,
+ encoderTightJPEG,
+ encoderZRLE,
+ encoderClassMax,
+};
+
+enum EncoderType {
+ encoderSolid,
+ encoderBitmap,
+ encoderBitmapRLE,
+ encoderIndexed,
+ encoderIndexedRLE,
+ encoderFullColour,
+ encoderTypeMax,
+};
+
+struct RectInfo {
+ int rleRuns;
+ Palette palette;
+};
+
+};
+
+EncodeManager::EncodeManager(SConnection* conn_) : conn(conn_)
+{
+ encoders.resize(encoderClassMax, NULL);
+ activeEncoders.resize(encoderTypeMax, encoderRaw);
+
+ encoders[encoderRaw] = new RawEncoder(conn);
+ encoders[encoderRRE] = new RREEncoder(conn);
+ encoders[encoderHextile] = new HextileEncoder(conn);
+ encoders[encoderTight] = new TightEncoder(conn);
+ encoders[encoderTightJPEG] = new TightJPEGEncoder(conn);
+ encoders[encoderZRLE] = new ZRLEEncoder(conn);
+}
+
+EncodeManager::~EncodeManager()
+{
+ std::vector<Encoder*>::iterator iter;
+
+ for (iter = encoders.begin();iter != encoders.end();iter++)
+ delete *iter;
+}
+
+bool EncodeManager::supported(int encoding)
+{
+ switch (encoding) {
+ case encodingRaw:
+ case encodingRRE:
+ case encodingHextile:
+ case encodingZRLE:
+ case encodingTight:
+ return true;
+ default:
+ return false;
+ }
+}
+
+void EncodeManager::writeUpdate(const UpdateInfo& ui, const PixelBuffer* pb,
+ const RenderedCursor* renderedCursor)
+{
+ int nRects;
+ Region changed;
+
+ prepareEncoders();
+
+ if (conn->cp.supportsLastRect)
+ nRects = 0xFFFF;
+ else {
+ nRects = ui.copied.numRects();
+ nRects += computeNumRects(ui.changed);
+
+ if (renderedCursor != NULL)
+ nRects += 1;
+ }
+
+ conn->writer()->writeFramebufferUpdateStart(nRects);
+
+ writeCopyRects(ui);
+
+ /*
+ * We start by searching for solid rects, which are then removed
+ * from the changed region.
+ */
+ changed.copyFrom(ui.changed);
+
+ if (conn->cp.supportsLastRect)
+ writeSolidRects(&changed, pb);
+
+ writeRects(changed, pb);
+
+ if (renderedCursor != NULL) {
+ Rect renderedCursorRect;
+
+ renderedCursorRect = renderedCursor->getEffectiveRect();
+ writeSubRect(renderedCursorRect, renderedCursor);
+ }
+
+ conn->writer()->writeFramebufferUpdateEnd();
+}
+
+void EncodeManager::prepareEncoders()
+{
+ enum EncoderClass solid, bitmap, bitmapRLE;
+ enum EncoderClass indexed, indexedRLE, fullColour;
+
+ rdr::S32 preferred;
+
+ std::vector<int>::iterator iter;
+
+ solid = bitmap = bitmapRLE = encoderRaw;
+ indexed = indexedRLE = fullColour = encoderRaw;
+
+ // Try to respect the client's wishes
+ preferred = conn->cp.preferredEncoding();
+ switch (preferred) {
+ case encodingRRE:
+ // Horrible for anything high frequency and/or lots of colours
+ bitmapRLE = indexedRLE = encoderRRE;
+ break;
+ case encodingHextile:
+ // Slightly less horrible
+ bitmapRLE = indexedRLE = fullColour = encoderHextile;
+ break;
+ case encodingTight:
+ if (encoders[encoderTightJPEG]->isSupported() &&
+ (conn->cp.pf().bpp >= 16))
+ fullColour = encoderTightJPEG;
+ else
+ fullColour = encoderTight;
+ indexed = indexedRLE = encoderTight;
+ bitmap = bitmapRLE = encoderTight;
+ break;
+ case encodingZRLE:
+ fullColour = encoderZRLE;
+ bitmapRLE = indexedRLE = encoderZRLE;
+ bitmap = indexed = encoderZRLE;
+ break;
+ }
+
+ // Any encoders still unassigned?
+
+ if (fullColour == encoderRaw) {
+ if (encoders[encoderTightJPEG]->isSupported() &&
+ (conn->cp.pf().bpp >= 16))
+ fullColour = encoderTightJPEG;
+ else if (encoders[encoderZRLE]->isSupported())
+ fullColour = encoderZRLE;
+ else if (encoders[encoderTight]->isSupported())
+ fullColour = encoderTight;
+ else if (encoders[encoderHextile]->isSupported())
+ fullColour = encoderHextile;
+ }
+
+ if (indexed == encoderRaw) {
+ if (encoders[encoderZRLE]->isSupported())
+ indexed = encoderZRLE;
+ else if (encoders[encoderTight]->isSupported())
+ indexed = encoderTight;
+ else if (encoders[encoderHextile]->isSupported())
+ indexed = encoderHextile;
+ }
+
+ if (indexedRLE == encoderRaw)
+ indexedRLE = indexed;
+
+ if (bitmap == encoderRaw)
+ bitmap = indexed;
+ if (bitmapRLE == encoderRaw)
+ bitmapRLE = bitmap;
+
+ if (solid == encoderRaw) {
+ if (encoders[encoderTight]->isSupported())
+ solid = encoderTight;
+ else if (encoders[encoderRRE]->isSupported())
+ solid = encoderRRE;
+ else if (encoders[encoderZRLE]->isSupported())
+ solid = encoderZRLE;
+ else if (encoders[encoderHextile]->isSupported())
+ solid = encoderHextile;
+ }
+
+ // JPEG is the only encoder that can reduce things to grayscale
+ if ((conn->cp.subsampling == subsampleGray) &&
+ encoders[encoderTightJPEG]->isSupported()) {
+ solid = bitmap = bitmapRLE = encoderTightJPEG;
+ indexed = indexedRLE = fullColour = encoderTightJPEG;
+ }
+
+ activeEncoders[encoderSolid] = solid;
+ activeEncoders[encoderBitmap] = bitmap;
+ activeEncoders[encoderBitmapRLE] = bitmapRLE;
+ activeEncoders[encoderIndexed] = indexed;
+ activeEncoders[encoderIndexedRLE] = indexedRLE;
+ activeEncoders[encoderFullColour] = fullColour;
+
+ for (iter = activeEncoders.begin(); iter != activeEncoders.end(); ++iter) {
+ Encoder *encoder;
+
+ encoder = encoders[*iter];
+
+ encoder->setCompressLevel(conn->cp.compressLevel);
+ encoder->setQualityLevel(conn->cp.qualityLevel);
+ encoder->setFineQualityLevel(conn->cp.fineQualityLevel,
+ conn->cp.subsampling);
+ }
+}
+
+int EncodeManager::computeNumRects(const Region& changed)
+{
+ int numRects;
+ std::vector<Rect> rects;
+ std::vector<Rect>::const_iterator rect;
+
+ numRects = 0;
+ changed.get_rects(&rects);
+ for (rect = rects.begin(); rect != rects.end(); ++rect) {
+ int w, h, sw, sh;
+
+ w = rect->width();
+ h = rect->height();
+
+ // No split necessary?
+ if (((w*h) < SubRectMaxArea) && (w < SubRectMaxWidth)) {
+ numRects += 1;
+ continue;
+ }
+
+ if (w <= SubRectMaxWidth)
+ sw = w;
+ else
+ sw = SubRectMaxWidth;
+
+ sh = SubRectMaxArea / sw;
+
+ // ceil(w/sw) * ceil(h/sh)
+ numRects += (((w - 1)/sw) + 1) * (((h - 1)/sh) + 1);
+ }
+
+ return numRects;
+}
+
+void EncodeManager::writeCopyRects(const UpdateInfo& ui)
+{
+ std::vector<Rect> rects;
+ std::vector<Rect>::const_iterator rect;
+
+ ui.copied.get_rects(&rects, ui.copy_delta.x <= 0, ui.copy_delta.y <= 0);
+ for (rect = rects.begin(); rect != rects.end(); ++rect) {
+ conn->writer()->writeCopyRect(*rect, rect->tl.x - ui.copy_delta.x,
+ rect->tl.y - ui.copy_delta.y);
+ }
+}
+
+void EncodeManager::writeSolidRects(Region *changed, const PixelBuffer* pb)
+{
+ std::vector<Rect> rects;
+ std::vector<Rect>::const_iterator rect;
+
+ // FIXME: This gives up after the first rect it finds. A large update
+ // (like a whole screen refresh) might have lots of large solid
+ // areas.
+
+ changed->get_rects(&rects);
+ for (rect = rects.begin(); rect != rects.end(); ++rect) {
+ Rect sr;
+ int dx, dy, dw, dh;
+
+ // We start by finding a solid 16x16 block
+ for (dy = rect->tl.y; dy < rect->br.y; dy += SolidSearchBlock) {
+
+ dh = SolidSearchBlock;
+ if (dy + dh > rect->br.y)
+ dh = rect->br.y - dy;
+
+ for (dx = rect->tl.x; dx < rect->br.x; dx += SolidSearchBlock) {
+ // We define it like this to guarantee alignment
+ rdr::U32 _buffer;
+ rdr::U8* colourValue = (rdr::U8*)&_buffer;
+
+ dw = SolidSearchBlock;
+ if (dx + dw > rect->br.x)
+ dw = rect->br.x - dx;
+
+ pb->getImage(colourValue, Rect(dx, dy, dx+1, dy+1));
+
+ sr.setXYWH(dx, dy, dw, dh);
+ if (checkSolidTile(sr, colourValue, pb)) {
+ Rect erb, erp;
+
+ Encoder *encoder;
+
+ // We then try extending the area by adding more blocks
+ // in both directions and pick the combination that gives
+ // the largest area.
+ sr.setXYWH(dx, dy, rect->br.x - dx, rect->br.y - dy);
+ extendSolidAreaByBlock(sr, colourValue, pb, &erb);
+
+ // Did we end up getting the entire rectangle?
+ if (erb.equals(*rect))
+ erp = erb;
+ else {
+ // Don't bother with sending tiny rectangles
+ if (erb.area() < SolidBlockMinArea)
+ continue;
+
+ // Extend the area again, but this time one pixel
+ // row/column at a time.
+ extendSolidAreaByPixel(*rect, erb, colourValue, pb, &erp);
+ }
+
+ // Send solid-color rectangle.
+ encoder = encoders[activeEncoders[encoderSolid]];
+ conn->writer()->startRect(erp, encoder->encoding);
+ if (encoder->flags & EncoderUseNativePF) {
+ encoder->writeSolidRect(erp.width(), erp.height(),
+ pb->getPF(), colourValue);
+ } else {
+ rdr::U32 _buffer2;
+ rdr::U8* converted = (rdr::U8*)&_buffer2;
+
+ conn->cp.pf().bufferFromBuffer(converted, pb->getPF(),
+ colourValue, 1);
+
+ encoder->writeSolidRect(erp.width(), erp.height(),
+ conn->cp.pf(), converted);
+ }
+ conn->writer()->endRect();
+
+ changed->assign_subtract(Region(erp));
+
+ break;
+ }
+ }
+
+ if (dx < rect->br.x)
+ break;
+ }
+ }
+}
+
+void EncodeManager::writeRects(const Region& changed, const PixelBuffer* pb)
+{
+ std::vector<Rect> rects;
+ std::vector<Rect>::const_iterator rect;
+
+ changed.get_rects(&rects);
+ for (rect = rects.begin(); rect != rects.end(); ++rect) {
+ int w, h, sw, sh;
+ Rect sr;
+
+ w = rect->width();
+ h = rect->height();
+
+ // No split necessary?
+ if (((w*h) < SubRectMaxArea) && (w < SubRectMaxWidth)) {
+ writeSubRect(*rect, pb);
+ continue;
+ }
+
+ if (w <= SubRectMaxWidth)
+ sw = w;
+ else
+ sw = SubRectMaxWidth;
+
+ sh = SubRectMaxArea / sw;
+
+ for (sr.tl.y = rect->tl.y; sr.tl.y < rect->br.y; sr.tl.y += sh) {
+ sr.br.y = sr.tl.y + sh;
+ if (sr.br.y > rect->br.y)
+ sr.br.y = rect->br.y;
+
+ for (sr.tl.x = rect->tl.x; sr.tl.x < rect->br.x; sr.tl.x += sw) {
+ sr.br.x = sr.tl.x + sw;
+ if (sr.br.x > rect->br.x)
+ sr.br.x = rect->br.x;
+
+ writeSubRect(sr, pb);
+ }
+ }
+ }
+}
+
+void EncodeManager::writeSubRect(const Rect& rect, const PixelBuffer *pb)
+{
+ PixelBuffer *ppb;
+
+ Encoder *encoder;
+
+ struct RectInfo info;
+ int divisor, maxColours;
+
+ bool useRLE;
+ EncoderType type;
+
+ // FIXME: This is roughly the algorithm previously used by the Tight
+ // encoder. It seems a bit backwards though, that higher
+ // compression setting means spending less effort in building
+ // a palette. It might be that they figured the increase in
+ // zlib setting compensated for the loss.
+ if (conn->cp.compressLevel == -1)
+ divisor = 2 * 8;
+ else
+ divisor = conn->cp.compressLevel * 8;
+ if (divisor < 4)
+ divisor = 4;
+
+ maxColours = rect.area()/divisor;
+
+ // Special exception inherited from the Tight encoder
+ if (activeEncoders[encoderFullColour] == encoderTightJPEG) {
+ if (conn->cp.compressLevel < 2)
+ maxColours = 24;
+ else
+ maxColours = 96;
+ }
+
+ if (maxColours < 2)
+ maxColours = 2;
+
+ encoder = encoders[activeEncoders[encoderIndexedRLE]];
+ if (maxColours > encoder->maxPaletteSize)
+ maxColours = encoder->maxPaletteSize;
+ encoder = encoders[activeEncoders[encoderIndexed]];
+ if (maxColours > encoder->maxPaletteSize)
+ maxColours = encoder->maxPaletteSize;
+
+ ppb = preparePixelBuffer(rect, pb, true);
+
+ if (!analyseRect(ppb, &info, maxColours))
+ info.palette.clear();
+
+ // Different encoders might have different RLE overhead, but
+ // here we do a guess at RLE being the better choice if reduces
+ // the pixel count by 50%.
+ useRLE = info.rleRuns <= (rect.area() * 2);
+
+ switch (info.palette.size()) {
+ case 0:
+ type = encoderFullColour;
+ break;
+ case 1:
+ type = encoderSolid;
+ break;
+ case 2:
+ if (useRLE)
+ type = encoderBitmapRLE;
+ else
+ type = encoderBitmap;
+ break;
+ default:
+ if (useRLE)
+ type = encoderIndexedRLE;
+ else
+ type = encoderIndexed;
+ }
+
+ encoder = encoders[activeEncoders[type]];
+
+ if (encoder->flags & EncoderUseNativePF)
+ ppb = preparePixelBuffer(rect, pb, false);
+
+ conn->writer()->startRect(rect, encoder->encoding);
+ encoder->writeRect(ppb, info.palette);
+ conn->writer()->endRect();
+}
+
+bool EncodeManager::checkSolidTile(const Rect& r, const rdr::U8* colourValue,
+ const PixelBuffer *pb)
+{
+ switch (pb->getPF().bpp) {
+ case 32:
+ return checkSolidTile(r, *(const rdr::U32*)colourValue, pb);
+ case 16:
+ return checkSolidTile(r, *(const rdr::U16*)colourValue, pb);
+ default:
+ return checkSolidTile(r, *(const rdr::U8*)colourValue, pb);
+ }
+}
+
+void EncodeManager::extendSolidAreaByBlock(const Rect& r,
+ const rdr::U8* colourValue,
+ const PixelBuffer *pb, Rect* er)
+{
+ int dx, dy, dw, dh;
+ int w_prev;
+ Rect sr;
+ int w_best = 0, h_best = 0;
+
+ w_prev = r.width();
+
+ // We search width first, back off when we hit a different colour,
+ // and restart with a larger height. We keep track of the
+ // width/height combination that gives us the largest area.
+ for (dy = r.tl.y; dy < r.br.y; dy += SolidSearchBlock) {
+
+ dh = SolidSearchBlock;
+ if (dy + dh > r.br.y)
+ dh = r.br.y - dy;
+
+ // We test one block here outside the x loop in order to break
+ // the y loop right away.
+ dw = SolidSearchBlock;
+ if (dw > w_prev)
+ dw = w_prev;
+
+ sr.setXYWH(r.tl.x, dy, dw, dh);
+ if (!checkSolidTile(sr, colourValue, pb))
+ break;
+
+ for (dx = r.tl.x + dw; dx < r.tl.x + w_prev;) {
+
+ dw = SolidSearchBlock;
+ if (dx + dw > r.tl.x + w_prev)
+ dw = r.tl.x + w_prev - dx;
+
+ sr.setXYWH(dx, dy, dw, dh);
+ if (!checkSolidTile(sr, colourValue, pb))
+ break;
+
+ dx += dw;
+ }
+
+ w_prev = dx - r.tl.x;
+ if (w_prev * (dy + dh - r.tl.y) > w_best * h_best) {
+ w_best = w_prev;
+ h_best = dy + dh - r.tl.y;
+ }
+ }
+
+ er->tl.x = r.tl.x;
+ er->tl.y = r.tl.y;
+ er->br.x = er->tl.x + w_best;
+ er->br.y = er->tl.y + h_best;
+}
+
+void EncodeManager::extendSolidAreaByPixel(const Rect& r, const Rect& sr,
+ const rdr::U8* colourValue,
+ const PixelBuffer *pb, Rect* er)
+{
+ int cx, cy;
+ Rect tr;
+
+ // Try to extend the area upwards.
+ for (cy = sr.tl.y - 1; cy >= r.tl.y; cy--) {
+ tr.setXYWH(sr.tl.x, cy, sr.width(), 1);
+ if (!checkSolidTile(tr, colourValue, pb))
+ break;
+ }
+ er->tl.y = cy + 1;
+
+ // ... downwards.
+ for (cy = sr.br.y; cy < r.br.y; cy++) {
+ tr.setXYWH(sr.tl.x, cy, sr.width(), 1);
+ if (!checkSolidTile(tr, colourValue, pb))
+ break;
+ }
+ er->br.y = cy;
+
+ // ... to the left.
+ for (cx = sr.tl.x - 1; cx >= r.tl.x; cx--) {
+ tr.setXYWH(cx, er->tl.y, 1, er->height());
+ if (!checkSolidTile(tr, colourValue, pb))
+ break;
+ }
+ er->tl.x = cx + 1;
+
+ // ... to the right.
+ for (cx = sr.br.x; cx < r.br.x; cx++) {
+ tr.setXYWH(cx, er->tl.y, 1, er->height());
+ if (!checkSolidTile(tr, colourValue, pb))
+ break;
+ }
+ er->br.x = cx;
+}
+
+PixelBuffer* EncodeManager::preparePixelBuffer(const Rect& rect,
+ const PixelBuffer *pb,
+ bool convert)
+{
+ const rdr::U8* buffer;
+ int stride;
+
+ // Do wo need to convert the data?
+ if (convert && !conn->cp.pf().equal(pb->getPF())) {
+ convertedPixelBuffer.setPF(conn->cp.pf());
+ convertedPixelBuffer.setSize(rect.width(), rect.height());
+
+ buffer = pb->getBuffer(rect, &stride);
+ convertedPixelBuffer.imageRect(pb->getPF(),
+ convertedPixelBuffer.getRect(),
+ buffer, stride);
+
+ return &convertedPixelBuffer;
+ }
+
+ // Otherwise we still need to shift the coordinates. We have our own
+ // abusive subclass of FullFramePixelBuffer for this.
+
+ buffer = pb->getBuffer(rect, &stride);
+
+ offsetPixelBuffer.update(pb->getPF(), rect.width(), rect.height(),
+ buffer, stride);
+
+ return &offsetPixelBuffer;
+}
+
+bool EncodeManager::analyseRect(const PixelBuffer *pb,
+ struct RectInfo *info, int maxColours)
+{
+ const rdr::U8* buffer;
+ int stride;
+
+ buffer = pb->getBuffer(pb->getRect(), &stride);
+
+ switch (pb->getPF().bpp) {
+ case 32:
+ return analyseRect(pb->width(), pb->height(),
+ (const rdr::U32*)buffer, stride,
+ info, maxColours);
+ case 16:
+ return analyseRect(pb->width(), pb->height(),
+ (const rdr::U16*)buffer, stride,
+ info, maxColours);
+ default:
+ return analyseRect(pb->width(), pb->height(),
+ (const rdr::U8*)buffer, stride,
+ info, maxColours);
+ }
+}
+
+void EncodeManager::OffsetPixelBuffer::update(const PixelFormat& pf,
+ int width, int height,
+ const rdr::U8* data_,
+ int stride_)
+{
+ format = pf;
+ width_ = width;
+ height_ = height;
+ // Forced cast. We never write anything though, so it should be safe.
+ data = (rdr::U8*)data_;
+ stride = stride_;
+}
+
+// Preprocessor generated, optimised methods
+
+#define BPP 8
+#include "EncodeManagerBPP.cxx"
+#undef BPP
+#define BPP 16
+#include "EncodeManagerBPP.cxx"
+#undef BPP
+#define BPP 32
+#include "EncodeManagerBPP.cxx"
+#undef BPP