Fixes: 27941148 Make OffscreenBuffer lifecycle an explicit (and tested) contract between FrameBuilder and BakedOpRenderer, entirely separate from dispatch. This makes it safe to reject any rendering work via overdraw content rejection (before it gets to a BakedOpDispatcher). Adds a couple tests around OffscreenBuffer leaks, and switches OffscreenBuffer tests to RENDERTHREAD_TEST macro, as appropriate. Change-Id: Id114b835d042708ae921028fb4b17e5fa485fe64
850 lines
39 KiB
C++
850 lines
39 KiB
C++
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "BakedOpDispatcher.h"
|
|
|
|
#include "BakedOpRenderer.h"
|
|
#include "Caches.h"
|
|
#include "Glop.h"
|
|
#include "GlopBuilder.h"
|
|
#include "Patch.h"
|
|
#include "PathTessellator.h"
|
|
#include "renderstate/OffscreenBufferPool.h"
|
|
#include "renderstate/RenderState.h"
|
|
#include "utils/GLUtils.h"
|
|
#include "VertexBuffer.h"
|
|
|
|
#include <algorithm>
|
|
#include <math.h>
|
|
#include <SkPaintDefaults.h>
|
|
#include <SkPathOps.h>
|
|
|
|
namespace android {
|
|
namespace uirenderer {
|
|
|
|
static void storeTexturedRect(TextureVertex* vertices, const Rect& bounds, const Rect& texCoord) {
|
|
vertices[0] = { bounds.left, bounds.top, texCoord.left, texCoord.top };
|
|
vertices[1] = { bounds.right, bounds.top, texCoord.right, texCoord.top };
|
|
vertices[2] = { bounds.left, bounds.bottom, texCoord.left, texCoord.bottom };
|
|
vertices[3] = { bounds.right, bounds.bottom, texCoord.right, texCoord.bottom };
|
|
}
|
|
|
|
void BakedOpDispatcher::onMergedBitmapOps(BakedOpRenderer& renderer,
|
|
const MergedBakedOpList& opList) {
|
|
|
|
const BakedOpState& firstState = *(opList.states[0]);
|
|
const SkBitmap* bitmap = (static_cast<const BitmapOp*>(opList.states[0]->op))->bitmap;
|
|
|
|
AssetAtlas::Entry* entry = renderer.renderState().assetAtlas().getEntry(bitmap->pixelRef());
|
|
Texture* texture = entry ? entry->texture : renderer.caches().textureCache.get(bitmap);
|
|
if (!texture) return;
|
|
const AutoTexture autoCleanup(texture);
|
|
|
|
TextureVertex vertices[opList.count * 4];
|
|
Rect texCoords(0, 0, 1, 1);
|
|
if (entry) {
|
|
entry->uvMapper.map(texCoords);
|
|
}
|
|
for (size_t i = 0; i < opList.count; i++) {
|
|
const BakedOpState& state = *(opList.states[i]);
|
|
TextureVertex* rectVerts = &vertices[i * 4];
|
|
|
|
// calculate unclipped bounds, since they'll determine texture coordinates
|
|
Rect opBounds = state.op->unmappedBounds;
|
|
state.computedState.transform.mapRect(opBounds);
|
|
if (CC_LIKELY(state.computedState.transform.isPureTranslate())) {
|
|
// pure translate, so snap (same behavior as onBitmapOp)
|
|
opBounds.snapToPixelBoundaries();
|
|
}
|
|
storeTexturedRect(rectVerts, opBounds, texCoords);
|
|
renderer.dirtyRenderTarget(opBounds);
|
|
}
|
|
|
|
const int textureFillFlags = (bitmap->colorType() == kAlpha_8_SkColorType)
|
|
? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(firstState.roundRectClipState)
|
|
.setMeshTexturedIndexedQuads(vertices, opList.count * 6)
|
|
.setFillTexturePaint(*texture, textureFillFlags, firstState.op->paint, firstState.alpha)
|
|
.setTransform(Matrix4::identity(), TransformFlags::None)
|
|
.setModelViewIdentityEmptyBounds()
|
|
.build();
|
|
ClipRect renderTargetClip(opList.clip);
|
|
const ClipBase* clip = opList.clipSideFlags ? &renderTargetClip : nullptr;
|
|
renderer.renderGlop(nullptr, clip, glop);
|
|
}
|
|
|
|
void BakedOpDispatcher::onMergedPatchOps(BakedOpRenderer& renderer,
|
|
const MergedBakedOpList& opList) {
|
|
const PatchOp& firstOp = *(static_cast<const PatchOp*>(opList.states[0]->op));
|
|
const BakedOpState& firstState = *(opList.states[0]);
|
|
AssetAtlas::Entry* entry = renderer.renderState().assetAtlas().getEntry(
|
|
firstOp.bitmap->pixelRef());
|
|
|
|
// Batches will usually contain a small number of items so it's
|
|
// worth performing a first iteration to count the exact number
|
|
// of vertices we need in the new mesh
|
|
uint32_t totalVertices = 0;
|
|
|
|
for (size_t i = 0; i < opList.count; i++) {
|
|
const PatchOp& op = *(static_cast<const PatchOp*>(opList.states[i]->op));
|
|
|
|
// TODO: cache mesh lookups
|
|
const Patch* opMesh = renderer.caches().patchCache.get(
|
|
entry, op.bitmap->width(), op.bitmap->height(),
|
|
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.patch);
|
|
totalVertices += opMesh->verticesCount;
|
|
}
|
|
|
|
const bool dirtyRenderTarget = renderer.offscreenRenderTarget();
|
|
|
|
uint32_t indexCount = 0;
|
|
|
|
TextureVertex vertices[totalVertices];
|
|
TextureVertex* vertex = &vertices[0];
|
|
// Create a mesh that contains the transformed vertices for all the
|
|
// 9-patch objects that are part of the batch. Note that onDefer()
|
|
// enforces ops drawn by this function to have a pure translate or
|
|
// identity matrix
|
|
for (size_t i = 0; i < opList.count; i++) {
|
|
const PatchOp& op = *(static_cast<const PatchOp*>(opList.states[i]->op));
|
|
const BakedOpState& state = *opList.states[i];
|
|
|
|
// TODO: cache mesh lookups
|
|
const Patch* opMesh = renderer.caches().patchCache.get(
|
|
entry, op.bitmap->width(), op.bitmap->height(),
|
|
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.patch);
|
|
|
|
|
|
uint32_t vertexCount = opMesh->verticesCount;
|
|
if (vertexCount == 0) continue;
|
|
|
|
// We use the bounds to know where to translate our vertices
|
|
// Using patchOp->state.mBounds wouldn't work because these
|
|
// bounds are clipped
|
|
const float tx = floorf(state.computedState.transform.getTranslateX()
|
|
+ op.unmappedBounds.left + 0.5f);
|
|
const float ty = floorf(state.computedState.transform.getTranslateY()
|
|
+ op.unmappedBounds.top + 0.5f);
|
|
|
|
// Copy & transform all the vertices for the current operation
|
|
TextureVertex* opVertices = opMesh->vertices.get();
|
|
for (uint32_t j = 0; j < vertexCount; j++, opVertices++) {
|
|
TextureVertex::set(vertex++,
|
|
opVertices->x + tx, opVertices->y + ty,
|
|
opVertices->u, opVertices->v);
|
|
}
|
|
|
|
// Dirty the current layer if possible. When the 9-patch does not
|
|
// contain empty quads we can take a shortcut and simply set the
|
|
// dirty rect to the object's bounds.
|
|
if (dirtyRenderTarget) {
|
|
if (!opMesh->hasEmptyQuads) {
|
|
renderer.dirtyRenderTarget(Rect(tx, ty,
|
|
tx + op.unmappedBounds.getWidth(), ty + op.unmappedBounds.getHeight()));
|
|
} else {
|
|
const size_t count = opMesh->quads.size();
|
|
for (size_t i = 0; i < count; i++) {
|
|
const Rect& quadBounds = opMesh->quads[i];
|
|
const float x = tx + quadBounds.left;
|
|
const float y = ty + quadBounds.top;
|
|
renderer.dirtyRenderTarget(Rect(x, y,
|
|
x + quadBounds.getWidth(), y + quadBounds.getHeight()));
|
|
}
|
|
}
|
|
}
|
|
|
|
indexCount += opMesh->indexCount;
|
|
}
|
|
|
|
|
|
Texture* texture = entry ? entry->texture : renderer.caches().textureCache.get(firstOp.bitmap);
|
|
if (!texture) return;
|
|
const AutoTexture autoCleanup(texture);
|
|
|
|
// 9 patches are built for stretching - always filter
|
|
int textureFillFlags = TextureFillFlags::ForceFilter;
|
|
if (firstOp.bitmap->colorType() == kAlpha_8_SkColorType) {
|
|
textureFillFlags |= TextureFillFlags::IsAlphaMaskTexture;
|
|
}
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(firstState.roundRectClipState)
|
|
.setMeshTexturedIndexedQuads(vertices, indexCount)
|
|
.setFillTexturePaint(*texture, textureFillFlags, firstOp.paint, firstState.alpha)
|
|
.setTransform(Matrix4::identity(), TransformFlags::None)
|
|
.setModelViewIdentityEmptyBounds()
|
|
.build();
|
|
ClipRect renderTargetClip(opList.clip);
|
|
const ClipBase* clip = opList.clipSideFlags ? &renderTargetClip : nullptr;
|
|
renderer.renderGlop(nullptr, clip, glop);
|
|
}
|
|
|
|
static void renderTextShadow(BakedOpRenderer& renderer, FontRenderer& fontRenderer,
|
|
const TextOp& op, const BakedOpState& textOpState) {
|
|
renderer.caches().textureState().activateTexture(0);
|
|
|
|
PaintUtils::TextShadow textShadow;
|
|
if (!PaintUtils::getTextShadow(op.paint, &textShadow)) {
|
|
LOG_ALWAYS_FATAL("failed to query shadow attributes");
|
|
}
|
|
|
|
renderer.caches().dropShadowCache.setFontRenderer(fontRenderer);
|
|
ShadowTexture* texture = renderer.caches().dropShadowCache.get(
|
|
op.paint, op.glyphs, op.glyphCount, textShadow.radius, op.positions);
|
|
// If the drop shadow exceeds the max texture size or couldn't be
|
|
// allocated, skip drawing
|
|
if (!texture) return;
|
|
const AutoTexture autoCleanup(texture);
|
|
|
|
const float sx = op.x - texture->left + textShadow.dx;
|
|
const float sy = op.y - texture->top + textShadow.dy;
|
|
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(textOpState.roundRectClipState)
|
|
.setMeshTexturedUnitQuad(nullptr)
|
|
.setFillShadowTexturePaint(*texture, textShadow.color, *op.paint, textOpState.alpha)
|
|
.setTransform(textOpState.computedState.transform, TransformFlags::None)
|
|
.setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width(), sy + texture->height()))
|
|
.build();
|
|
|
|
// Compute damage bounds and clip (since may differ from those in textOpState).
|
|
// Bounds should be same as text op, but with dx/dy offset and radius outset
|
|
// applied in local space.
|
|
auto& transform = textOpState.computedState.transform;
|
|
Rect shadowBounds = op.unmappedBounds; // STROKE
|
|
const bool expandForStroke = op.paint->getStyle() != SkPaint::kFill_Style;
|
|
if (expandForStroke) {
|
|
shadowBounds.outset(op.paint->getStrokeWidth() * 0.5f);
|
|
}
|
|
shadowBounds.translate(textShadow.dx, textShadow.dy);
|
|
shadowBounds.outset(textShadow.radius, textShadow.radius);
|
|
transform.mapRect(shadowBounds);
|
|
if (CC_UNLIKELY(expandForStroke &&
|
|
(!transform.isPureTranslate() || op.paint->getStrokeWidth() < 1.0f))) {
|
|
shadowBounds.outset(0.5f);
|
|
}
|
|
|
|
auto clipState = textOpState.computedState.clipState;
|
|
if (clipState->mode != ClipMode::Rectangle
|
|
|| !clipState->rect.contains(shadowBounds)) {
|
|
// need clip, so pass it and clip bounds
|
|
shadowBounds.doIntersect(clipState->rect);
|
|
} else {
|
|
// don't need clip, ignore
|
|
clipState = nullptr;
|
|
}
|
|
|
|
renderer.renderGlop(&shadowBounds, clipState, glop);
|
|
}
|
|
|
|
enum class TextRenderType {
|
|
Defer,
|
|
Flush
|
|
};
|
|
|
|
static void renderTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state,
|
|
const ClipBase* renderClip, TextRenderType renderType) {
|
|
FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer();
|
|
|
|
if (CC_UNLIKELY(PaintUtils::hasTextShadow(op.paint))) {
|
|
fontRenderer.setFont(op.paint, SkMatrix::I());
|
|
renderTextShadow(renderer, fontRenderer, op, state);
|
|
}
|
|
|
|
float x = op.x;
|
|
float y = op.y;
|
|
const Matrix4& transform = state.computedState.transform;
|
|
const bool pureTranslate = transform.isPureTranslate();
|
|
if (CC_LIKELY(pureTranslate)) {
|
|
x = floorf(x + transform.getTranslateX() + 0.5f);
|
|
y = floorf(y + transform.getTranslateY() + 0.5f);
|
|
fontRenderer.setFont(op.paint, SkMatrix::I());
|
|
fontRenderer.setTextureFiltering(false);
|
|
} else if (CC_UNLIKELY(transform.isPerspective())) {
|
|
fontRenderer.setFont(op.paint, SkMatrix::I());
|
|
fontRenderer.setTextureFiltering(true);
|
|
} else {
|
|
// We only pass a partial transform to the font renderer. That partial
|
|
// matrix defines how glyphs are rasterized. Typically we want glyphs
|
|
// to be rasterized at their final size on screen, which means the partial
|
|
// matrix needs to take the scale factor into account.
|
|
// When a partial matrix is used to transform glyphs during rasterization,
|
|
// the mesh is generated with the inverse transform (in the case of scale,
|
|
// the mesh is generated at 1.0 / scale for instance.) This allows us to
|
|
// apply the full transform matrix at draw time in the vertex shader.
|
|
// Applying the full matrix in the shader is the easiest way to handle
|
|
// rotation and perspective and allows us to always generated quads in the
|
|
// font renderer which greatly simplifies the code, clipping in particular.
|
|
float sx, sy;
|
|
transform.decomposeScale(sx, sy);
|
|
fontRenderer.setFont(op.paint, SkMatrix::MakeScale(
|
|
roundf(std::max(1.0f, sx)),
|
|
roundf(std::max(1.0f, sy))));
|
|
fontRenderer.setTextureFiltering(true);
|
|
}
|
|
Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
|
|
|
|
int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha;
|
|
SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint);
|
|
TextDrawFunctor functor(&renderer, &state, renderClip,
|
|
x, y, pureTranslate, alpha, mode, op.paint);
|
|
|
|
bool forceFinish = (renderType == TextRenderType::Flush);
|
|
bool mustDirtyRenderTarget = renderer.offscreenRenderTarget();
|
|
const Rect* localOpClip = pureTranslate ? &state.computedState.clipRect() : nullptr;
|
|
fontRenderer.renderPosText(op.paint, localOpClip, op.glyphs, op.glyphCount, x, y,
|
|
op.positions, mustDirtyRenderTarget ? &layerBounds : nullptr, &functor, forceFinish);
|
|
|
|
if (mustDirtyRenderTarget) {
|
|
if (!pureTranslate) {
|
|
transform.mapRect(layerBounds);
|
|
}
|
|
renderer.dirtyRenderTarget(layerBounds);
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onMergedTextOps(BakedOpRenderer& renderer,
|
|
const MergedBakedOpList& opList) {
|
|
ClipRect renderTargetClip(opList.clip);
|
|
const ClipBase* clip = opList.clipSideFlags ? &renderTargetClip : nullptr;
|
|
for (size_t i = 0; i < opList.count; i++) {
|
|
const BakedOpState& state = *(opList.states[i]);
|
|
const TextOp& op = *(static_cast<const TextOp*>(state.op));
|
|
TextRenderType renderType = (i + 1 == opList.count)
|
|
? TextRenderType::Flush : TextRenderType::Defer;
|
|
renderTextOp(renderer, op, state, clip, renderType);
|
|
}
|
|
}
|
|
|
|
namespace VertexBufferRenderFlags {
|
|
enum {
|
|
Offset = 0x1,
|
|
ShadowInterp = 0x2,
|
|
};
|
|
}
|
|
|
|
static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state,
|
|
const VertexBuffer& vertexBuffer, float translateX, float translateY,
|
|
const SkPaint& paint, int vertexBufferRenderFlags) {
|
|
if (CC_LIKELY(vertexBuffer.getVertexCount())) {
|
|
bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp;
|
|
const int transformFlags = TransformFlags::OffsetByFudgeFactor;
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshVertexBuffer(vertexBuffer, shadowInterp)
|
|
.setFillPaint(paint, state.alpha)
|
|
.setTransform(state.computedState.transform, transformFlags)
|
|
.setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
}
|
|
|
|
static void renderConvexPath(BakedOpRenderer& renderer, const BakedOpState& state,
|
|
const SkPath& path, const SkPaint& paint) {
|
|
VertexBuffer vertexBuffer;
|
|
// TODO: try clipping large paths to viewport
|
|
PathTessellator::tessellatePath(path, &paint, state.computedState.transform, vertexBuffer);
|
|
renderVertexBuffer(renderer, state, vertexBuffer, 0.0f, 0.0f, paint, 0);
|
|
}
|
|
|
|
static void renderPathTexture(BakedOpRenderer& renderer, const BakedOpState& state,
|
|
float xOffset, float yOffset, PathTexture& texture, const SkPaint& paint) {
|
|
Rect dest(texture.width(), texture.height());
|
|
dest.translate(xOffset + texture.left - texture.offset,
|
|
yOffset + texture.top - texture.offset);
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshTexturedUnitQuad(nullptr)
|
|
.setFillPathTexturePaint(texture, paint, state.alpha)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewMapUnitToRect(dest)
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
|
|
SkRect getBoundsOfFill(const RecordedOp& op) {
|
|
SkRect bounds = op.unmappedBounds.toSkRect();
|
|
if (op.paint->getStyle() == SkPaint::kStrokeAndFill_Style) {
|
|
float outsetDistance = op.paint->getStrokeWidth() / 2;
|
|
bounds.outset(outsetDistance, outsetDistance);
|
|
}
|
|
return bounds;
|
|
}
|
|
|
|
void BakedOpDispatcher::onArcOp(BakedOpRenderer& renderer, const ArcOp& op, const BakedOpState& state) {
|
|
// TODO: support fills (accounting for concavity if useCenter && sweepAngle > 180)
|
|
if (op.paint->getStyle() != SkPaint::kStroke_Style
|
|
|| op.paint->getPathEffect() != nullptr
|
|
|| op.useCenter) {
|
|
PathTexture* texture = renderer.caches().pathCache.getArc(
|
|
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(),
|
|
op.startAngle, op.sweepAngle, op.useCenter, op.paint);
|
|
const AutoTexture holder(texture);
|
|
if (CC_LIKELY(holder.texture)) {
|
|
renderPathTexture(renderer, state, op.unmappedBounds.left, op.unmappedBounds.top,
|
|
*texture, *(op.paint));
|
|
}
|
|
} else {
|
|
SkRect rect = getBoundsOfFill(op);
|
|
SkPath path;
|
|
if (op.useCenter) {
|
|
path.moveTo(rect.centerX(), rect.centerY());
|
|
}
|
|
path.arcTo(rect, op.startAngle, op.sweepAngle, !op.useCenter);
|
|
if (op.useCenter) {
|
|
path.close();
|
|
}
|
|
renderConvexPath(renderer, state, path, *(op.paint));
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op, const BakedOpState& state) {
|
|
Texture* texture = renderer.getTexture(op.bitmap);
|
|
if (!texture) return;
|
|
const AutoTexture autoCleanup(texture);
|
|
|
|
const int textureFillFlags = (op.bitmap->colorType() == kAlpha_8_SkColorType)
|
|
? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshTexturedUnitQuad(texture->uvMapper)
|
|
.setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewMapUnitToRectSnap(Rect(texture->width(), texture->height()))
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
|
|
void BakedOpDispatcher::onBitmapMeshOp(BakedOpRenderer& renderer, const BitmapMeshOp& op, const BakedOpState& state) {
|
|
const static UvMapper defaultUvMapper;
|
|
const uint32_t elementCount = op.meshWidth * op.meshHeight * 6;
|
|
|
|
std::unique_ptr<ColorTextureVertex[]> mesh(new ColorTextureVertex[elementCount]);
|
|
ColorTextureVertex* vertex = &mesh[0];
|
|
|
|
const int* colors = op.colors;
|
|
std::unique_ptr<int[]> tempColors;
|
|
if (!colors) {
|
|
uint32_t colorsCount = (op.meshWidth + 1) * (op.meshHeight + 1);
|
|
tempColors.reset(new int[colorsCount]);
|
|
memset(tempColors.get(), 0xff, colorsCount * sizeof(int));
|
|
colors = tempColors.get();
|
|
}
|
|
|
|
Texture* texture = renderer.renderState().assetAtlas().getEntryTexture(op.bitmap->pixelRef());
|
|
const UvMapper& mapper(texture && texture->uvMapper ? *texture->uvMapper : defaultUvMapper);
|
|
|
|
for (int32_t y = 0; y < op.meshHeight; y++) {
|
|
for (int32_t x = 0; x < op.meshWidth; x++) {
|
|
uint32_t i = (y * (op.meshWidth + 1) + x) * 2;
|
|
|
|
float u1 = float(x) / op.meshWidth;
|
|
float u2 = float(x + 1) / op.meshWidth;
|
|
float v1 = float(y) / op.meshHeight;
|
|
float v2 = float(y + 1) / op.meshHeight;
|
|
|
|
mapper.map(u1, v1, u2, v2);
|
|
|
|
int ax = i + (op.meshWidth + 1) * 2;
|
|
int ay = ax + 1;
|
|
int bx = i;
|
|
int by = bx + 1;
|
|
int cx = i + 2;
|
|
int cy = cx + 1;
|
|
int dx = i + (op.meshWidth + 1) * 2 + 2;
|
|
int dy = dx + 1;
|
|
|
|
const float* vertices = op.vertices;
|
|
ColorTextureVertex::set(vertex++, vertices[dx], vertices[dy], u2, v2, colors[dx / 2]);
|
|
ColorTextureVertex::set(vertex++, vertices[ax], vertices[ay], u1, v2, colors[ax / 2]);
|
|
ColorTextureVertex::set(vertex++, vertices[bx], vertices[by], u1, v1, colors[bx / 2]);
|
|
|
|
ColorTextureVertex::set(vertex++, vertices[dx], vertices[dy], u2, v2, colors[dx / 2]);
|
|
ColorTextureVertex::set(vertex++, vertices[bx], vertices[by], u1, v1, colors[bx / 2]);
|
|
ColorTextureVertex::set(vertex++, vertices[cx], vertices[cy], u2, v1, colors[cx / 2]);
|
|
}
|
|
}
|
|
|
|
if (!texture) {
|
|
texture = renderer.caches().textureCache.get(op.bitmap);
|
|
if (!texture) {
|
|
return;
|
|
}
|
|
}
|
|
const AutoTexture autoCleanup(texture);
|
|
|
|
/*
|
|
* TODO: handle alpha_8 textures correctly by applying paint color, but *not*
|
|
* shader in that case to mimic the behavior in SkiaCanvas::drawBitmapMesh.
|
|
*/
|
|
const int textureFillFlags = TextureFillFlags::None;
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshColoredTexturedMesh(mesh.get(), elementCount)
|
|
.setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewOffsetRect(0, 0, op.unmappedBounds)
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
|
|
void BakedOpDispatcher::onBitmapRectOp(BakedOpRenderer& renderer, const BitmapRectOp& op, const BakedOpState& state) {
|
|
Texture* texture = renderer.getTexture(op.bitmap);
|
|
if (!texture) return;
|
|
const AutoTexture autoCleanup(texture);
|
|
|
|
Rect uv(std::max(0.0f, op.src.left / texture->width()),
|
|
std::max(0.0f, op.src.top / texture->height()),
|
|
std::min(1.0f, op.src.right / texture->width()),
|
|
std::min(1.0f, op.src.bottom / texture->height()));
|
|
|
|
const int textureFillFlags = (op.bitmap->colorType() == kAlpha_8_SkColorType)
|
|
? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
|
|
const bool tryToSnap = MathUtils::areEqual(op.src.getWidth(), op.unmappedBounds.getWidth())
|
|
&& MathUtils::areEqual(op.src.getHeight(), op.unmappedBounds.getHeight());
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshTexturedUvQuad(texture->uvMapper, uv)
|
|
.setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewMapUnitToRectOptionalSnap(tryToSnap, op.unmappedBounds)
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
|
|
void BakedOpDispatcher::onColorOp(BakedOpRenderer& renderer, const ColorOp& op, const BakedOpState& state) {
|
|
SkPaint paint;
|
|
paint.setColor(op.color);
|
|
paint.setXfermodeMode(op.mode);
|
|
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshUnitQuad()
|
|
.setFillPaint(paint, state.alpha)
|
|
.setTransform(Matrix4::identity(), TransformFlags::None)
|
|
.setModelViewMapUnitToRect(state.computedState.clipState->rect)
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
|
|
void BakedOpDispatcher::onFunctorOp(BakedOpRenderer& renderer, const FunctorOp& op, const BakedOpState& state) {
|
|
renderer.renderFunctor(op, state);
|
|
}
|
|
|
|
void BakedOpDispatcher::onLinesOp(BakedOpRenderer& renderer, const LinesOp& op, const BakedOpState& state) {
|
|
VertexBuffer buffer;
|
|
PathTessellator::tessellateLines(op.points, op.floatCount, op.paint,
|
|
state.computedState.transform, buffer);
|
|
int displayFlags = op.paint->isAntiAlias() ? 0 : VertexBufferRenderFlags::Offset;
|
|
renderVertexBuffer(renderer, state, buffer, 0, 0, *(op.paint), displayFlags);
|
|
}
|
|
|
|
void BakedOpDispatcher::onOvalOp(BakedOpRenderer& renderer, const OvalOp& op, const BakedOpState& state) {
|
|
if (op.paint->getPathEffect() != nullptr) {
|
|
PathTexture* texture = renderer.caches().pathCache.getOval(
|
|
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.paint);
|
|
const AutoTexture holder(texture);
|
|
if (CC_LIKELY(holder.texture)) {
|
|
renderPathTexture(renderer, state, op.unmappedBounds.left, op.unmappedBounds.right,
|
|
*texture, *(op.paint));
|
|
}
|
|
} else {
|
|
SkPath path;
|
|
SkRect rect = getBoundsOfFill(op);
|
|
path.addOval(rect);
|
|
|
|
if (state.computedState.localProjectionPathMask != nullptr) {
|
|
// Mask the ripple path by the local space projection mask in local space.
|
|
// Note that this can create CCW paths.
|
|
Op(path, *state.computedState.localProjectionPathMask, kIntersect_SkPathOp, &path);
|
|
}
|
|
renderConvexPath(renderer, state, path, *(op.paint));
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onPatchOp(BakedOpRenderer& renderer, const PatchOp& op, const BakedOpState& state) {
|
|
// 9 patches are built for stretching - always filter
|
|
int textureFillFlags = TextureFillFlags::ForceFilter;
|
|
if (op.bitmap->colorType() == kAlpha_8_SkColorType) {
|
|
textureFillFlags |= TextureFillFlags::IsAlphaMaskTexture;
|
|
}
|
|
|
|
// TODO: avoid redoing the below work each frame:
|
|
AssetAtlas::Entry* entry = renderer.renderState().assetAtlas().getEntry(op.bitmap->pixelRef());
|
|
const Patch* mesh = renderer.caches().patchCache.get(
|
|
entry, op.bitmap->width(), op.bitmap->height(),
|
|
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.patch);
|
|
|
|
Texture* texture = entry ? entry->texture : renderer.caches().textureCache.get(op.bitmap);
|
|
if (CC_LIKELY(texture)) {
|
|
const AutoTexture autoCleanup(texture);
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshPatchQuads(*mesh)
|
|
.setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewOffsetRectSnap(op.unmappedBounds.left, op.unmappedBounds.top,
|
|
Rect(op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight()))
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onPathOp(BakedOpRenderer& renderer, const PathOp& op, const BakedOpState& state) {
|
|
PathTexture* texture = renderer.caches().pathCache.get(op.path, op.paint);
|
|
const AutoTexture holder(texture);
|
|
if (CC_LIKELY(holder.texture)) {
|
|
// Unlike other callers to renderPathTexture, no offsets are used because PathOp doesn't
|
|
// have any translate built in, other than what's in the SkPath itself
|
|
renderPathTexture(renderer, state, 0, 0, *texture, *(op.paint));
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onPointsOp(BakedOpRenderer& renderer, const PointsOp& op, const BakedOpState& state) {
|
|
VertexBuffer buffer;
|
|
PathTessellator::tessellatePoints(op.points, op.floatCount, op.paint,
|
|
state.computedState.transform, buffer);
|
|
int displayFlags = op.paint->isAntiAlias() ? 0 : VertexBufferRenderFlags::Offset;
|
|
renderVertexBuffer(renderer, state, buffer, 0, 0, *(op.paint), displayFlags);
|
|
}
|
|
|
|
// See SkPaintDefaults.h
|
|
#define SkPaintDefaults_MiterLimit SkIntToScalar(4)
|
|
|
|
void BakedOpDispatcher::onRectOp(BakedOpRenderer& renderer, const RectOp& op, const BakedOpState& state) {
|
|
if (op.paint->getStyle() != SkPaint::kFill_Style) {
|
|
// only fill + default miter is supported by drawConvexPath, since others must handle joins
|
|
static_assert(SkPaintDefaults_MiterLimit == 4.0f, "Miter limit has changed");
|
|
if (CC_UNLIKELY(op.paint->getPathEffect() != nullptr
|
|
|| op.paint->getStrokeJoin() != SkPaint::kMiter_Join
|
|
|| op.paint->getStrokeMiter() != SkPaintDefaults_MiterLimit)) {
|
|
PathTexture* texture = renderer.caches().pathCache.getRect(
|
|
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.paint);
|
|
const AutoTexture holder(texture);
|
|
if (CC_LIKELY(holder.texture)) {
|
|
renderPathTexture(renderer, state, op.unmappedBounds.left, op.unmappedBounds.top,
|
|
*texture, *(op.paint));
|
|
}
|
|
} else {
|
|
SkPath path;
|
|
path.addRect(getBoundsOfFill(op));
|
|
renderConvexPath(renderer, state, path, *(op.paint));
|
|
}
|
|
} else {
|
|
if (op.paint->isAntiAlias() && !state.computedState.transform.isSimple()) {
|
|
SkPath path;
|
|
path.addRect(op.unmappedBounds.toSkRect());
|
|
renderConvexPath(renderer, state, path, *(op.paint));
|
|
} else {
|
|
// render simple unit quad, no tessellation required
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshUnitQuad()
|
|
.setFillPaint(*op.paint, state.alpha)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewMapUnitToRect(op.unmappedBounds)
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onRoundRectOp(BakedOpRenderer& renderer, const RoundRectOp& op, const BakedOpState& state) {
|
|
if (op.paint->getPathEffect() != nullptr) {
|
|
PathTexture* texture = renderer.caches().pathCache.getRoundRect(
|
|
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(),
|
|
op.rx, op.ry, op.paint);
|
|
const AutoTexture holder(texture);
|
|
if (CC_LIKELY(holder.texture)) {
|
|
renderPathTexture(renderer, state, op.unmappedBounds.left, op.unmappedBounds.top,
|
|
*texture, *(op.paint));
|
|
}
|
|
} else {
|
|
const VertexBuffer* buffer = renderer.caches().tessellationCache.getRoundRect(
|
|
state.computedState.transform, *(op.paint),
|
|
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.rx, op.ry);
|
|
renderVertexBuffer(renderer, state, *buffer,
|
|
op.unmappedBounds.left, op.unmappedBounds.top, *(op.paint), 0);
|
|
}
|
|
}
|
|
|
|
static void renderShadow(BakedOpRenderer& renderer, const BakedOpState& state, float casterAlpha,
|
|
const VertexBuffer* ambientShadowVertexBuffer, const VertexBuffer* spotShadowVertexBuffer) {
|
|
SkPaint paint;
|
|
paint.setAntiAlias(true); // want to use AlphaVertex
|
|
|
|
// The caller has made sure casterAlpha > 0.
|
|
uint8_t ambientShadowAlpha = renderer.getLightInfo().ambientShadowAlpha;
|
|
if (CC_UNLIKELY(Properties::overrideAmbientShadowStrength >= 0)) {
|
|
ambientShadowAlpha = Properties::overrideAmbientShadowStrength;
|
|
}
|
|
if (ambientShadowVertexBuffer && ambientShadowAlpha > 0) {
|
|
paint.setAlpha((uint8_t)(casterAlpha * ambientShadowAlpha));
|
|
renderVertexBuffer(renderer, state, *ambientShadowVertexBuffer, 0, 0,
|
|
paint, VertexBufferRenderFlags::ShadowInterp);
|
|
}
|
|
|
|
uint8_t spotShadowAlpha = renderer.getLightInfo().spotShadowAlpha;
|
|
if (CC_UNLIKELY(Properties::overrideSpotShadowStrength >= 0)) {
|
|
spotShadowAlpha = Properties::overrideSpotShadowStrength;
|
|
}
|
|
if (spotShadowVertexBuffer && spotShadowAlpha > 0) {
|
|
paint.setAlpha((uint8_t)(casterAlpha * spotShadowAlpha));
|
|
renderVertexBuffer(renderer, state, *spotShadowVertexBuffer, 0, 0,
|
|
paint, VertexBufferRenderFlags::ShadowInterp);
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onShadowOp(BakedOpRenderer& renderer, const ShadowOp& op, const BakedOpState& state) {
|
|
TessellationCache::vertexBuffer_pair_t buffers = op.shadowTask->getResult();
|
|
renderShadow(renderer, state, op.casterAlpha, buffers.first, buffers.second);
|
|
}
|
|
|
|
void BakedOpDispatcher::onSimpleRectsOp(BakedOpRenderer& renderer, const SimpleRectsOp& op, const BakedOpState& state) {
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshIndexedQuads(&op.vertices[0], op.vertexCount / 4)
|
|
.setFillPaint(*op.paint, state.alpha)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewOffsetRect(0, 0, op.unmappedBounds)
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
|
|
void BakedOpDispatcher::onTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state) {
|
|
renderTextOp(renderer, op, state, state.computedState.getClipIfNeeded(), TextRenderType::Flush);
|
|
}
|
|
|
|
void BakedOpDispatcher::onTextOnPathOp(BakedOpRenderer& renderer, const TextOnPathOp& op, const BakedOpState& state) {
|
|
// Note: can't trust clipSideFlags since we record with unmappedBounds == clip.
|
|
// TODO: respect clipSideFlags, once we record with bounds
|
|
auto renderTargetClip = state.computedState.clipState;
|
|
|
|
FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer();
|
|
fontRenderer.setFont(op.paint, SkMatrix::I());
|
|
fontRenderer.setTextureFiltering(true);
|
|
|
|
Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
|
|
|
|
int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha;
|
|
SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint);
|
|
TextDrawFunctor functor(&renderer, &state, renderTargetClip,
|
|
0.0f, 0.0f, false, alpha, mode, op.paint);
|
|
|
|
bool mustDirtyRenderTarget = renderer.offscreenRenderTarget();
|
|
const Rect localSpaceClip = state.computedState.computeLocalSpaceClip();
|
|
if (fontRenderer.renderTextOnPath(op.paint, &localSpaceClip, op.glyphs, op.glyphCount,
|
|
op.path, op.hOffset, op.vOffset,
|
|
mustDirtyRenderTarget ? &layerBounds : nullptr, &functor)) {
|
|
if (mustDirtyRenderTarget) {
|
|
// manually dirty render target, since TextDrawFunctor won't
|
|
state.computedState.transform.mapRect(layerBounds);
|
|
renderer.dirtyRenderTarget(layerBounds);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onTextureLayerOp(BakedOpRenderer& renderer, const TextureLayerOp& op, const BakedOpState& state) {
|
|
const bool tryToSnap = !op.layer->getForceFilter();
|
|
float alpha = (op.layer->getAlpha() / 255.0f) * state.alpha;
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshTexturedUvQuad(nullptr, Rect(0, 1, 1, 0)) // TODO: simplify with VBO
|
|
.setFillTextureLayer(*(op.layer), alpha)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewMapUnitToRectOptionalSnap(tryToSnap, Rect(op.layer->getWidth(), op.layer->getHeight()))
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
|
|
void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) {
|
|
// Note that we don't use op->paint in this function - it's never set on a LayerOp
|
|
OffscreenBuffer* buffer = *op.layerHandle;
|
|
|
|
if (CC_UNLIKELY(!buffer)) {
|
|
// Layer was not allocated, which can occur if there were no draw ops inside. We draw the
|
|
// equivalent by drawing a rect with the same layer properties (alpha/xfer/filter).
|
|
SkPaint paint;
|
|
paint.setAlpha(op.alpha * 255);
|
|
paint.setXfermodeMode(op.mode);
|
|
paint.setColorFilter(op.colorFilter);
|
|
RectOp rectOp(op.unmappedBounds, op.localMatrix, op.localClip, &paint);
|
|
BakedOpDispatcher::onRectOp(renderer, rectOp, state);
|
|
} else {
|
|
float layerAlpha = op.alpha * state.alpha;
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshTexturedIndexedVbo(buffer->vbo, buffer->elementCount)
|
|
.setFillLayer(buffer->texture, op.colorFilter, layerAlpha, op.mode, Blend::ModeOrderSwap::NoSwap)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewOffsetRectSnap(op.unmappedBounds.left, op.unmappedBounds.top,
|
|
Rect(op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight()))
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
}
|
|
|
|
void BakedOpDispatcher::onCopyToLayerOp(BakedOpRenderer& renderer, const CopyToLayerOp& op, const BakedOpState& state) {
|
|
LOG_ALWAYS_FATAL_IF(*(op.layerHandle) != nullptr, "layer already exists!");
|
|
*(op.layerHandle) = renderer.copyToLayer(state.computedState.clippedBounds);
|
|
LOG_ALWAYS_FATAL_IF(*op.layerHandle == nullptr, "layer copy failed");
|
|
}
|
|
|
|
void BakedOpDispatcher::onCopyFromLayerOp(BakedOpRenderer& renderer, const CopyFromLayerOp& op, const BakedOpState& state) {
|
|
LOG_ALWAYS_FATAL_IF(*op.layerHandle == nullptr, "no layer to draw underneath!");
|
|
if (!state.computedState.clippedBounds.isEmpty()) {
|
|
if (op.paint && op.paint->getAlpha() < 255) {
|
|
SkPaint layerPaint;
|
|
layerPaint.setAlpha(op.paint->getAlpha());
|
|
layerPaint.setXfermodeMode(SkXfermode::kDstIn_Mode);
|
|
layerPaint.setColorFilter(op.paint->getColorFilter());
|
|
RectOp rectOp(state.computedState.clippedBounds, Matrix4::identity(), nullptr, &layerPaint);
|
|
BakedOpDispatcher::onRectOp(renderer, rectOp, state);
|
|
}
|
|
|
|
OffscreenBuffer& layer = **(op.layerHandle);
|
|
auto mode = PaintUtils::getXfermodeDirect(op.paint);
|
|
Glop glop;
|
|
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
|
|
.setRoundRectClipState(state.roundRectClipState)
|
|
.setMeshTexturedUvQuad(nullptr, layer.getTextureCoordinates())
|
|
.setFillLayer(layer.texture, nullptr, 1.0f, mode, Blend::ModeOrderSwap::Swap)
|
|
.setTransform(state.computedState.transform, TransformFlags::None)
|
|
.setModelViewMapUnitToRect(state.computedState.clippedBounds)
|
|
.build();
|
|
renderer.renderGlop(state, glop);
|
|
}
|
|
renderer.renderState().layerPool().putOrDelete(*op.layerHandle);
|
|
}
|
|
|
|
} // namespace uirenderer
|
|
} // namespace android
|