/* * Copyright 2023 Google LLC * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "src/gpu/graphite/render/CoverageMaskRenderStep.h" #include "include/core/SkM44.h" #include "include/core/SkMatrix.h" #include "include/core/SkRefCnt.h" #include "include/core/SkSamplingOptions.h" #include "include/core/SkScalar.h" #include "include/core/SkSize.h" #include "include/core/SkTileMode.h" #include "include/private/base/SkAssert.h" #include "include/private/base/SkDebug.h" #include "src/base/SkEnumBitMask.h" #include "src/core/SkSLTypeShared.h" #include "src/gpu/BufferWriter.h" #include "src/gpu/graphite/Attribute.h" #include "src/gpu/graphite/ContextUtils.h" #include "src/gpu/graphite/DrawOrder.h" #include "src/gpu/graphite/DrawParams.h" #include "src/gpu/graphite/DrawTypes.h" #include "src/gpu/graphite/DrawWriter.h" #include "src/gpu/graphite/PipelineData.h" #include "src/gpu/graphite/TextureProxy.h" #include "src/gpu/graphite/geom/CoverageMaskShape.h" #include "src/gpu/graphite/geom/Geometry.h" #include "src/gpu/graphite/geom/Rect.h" #include "src/gpu/graphite/geom/Transform_graphite.h" #include "src/gpu/graphite/render/CommonDepthStencilSettings.h" #include #include namespace skgpu::graphite { // The device origin is applied *before* the maskToDeviceRemainder matrix so that it can be // combined with the mask atlas origin. This is necessary so that the mask bounds can be inset or // outset for clamping w/o affecting the alignment of the mask sampling. static skvx::float2 get_device_translation(const SkM44& localToDevice) { float m00 = localToDevice.rc(0,0), m01 = localToDevice.rc(0,1); float m10 = localToDevice.rc(1,0), m11 = localToDevice.rc(1,1); float det = m00*m11 - m01*m10; if (SkScalarNearlyZero(det)) { // We can't extract any pre-translation, since the upper 2x2 is not invertible. Return (0,0) // so that the maskToDeviceRemainder matrix remains the full transform. return {0.f, 0.f}; } // Calculate inv([[m00,m01][m10,m11]])*[[m30][m31]] to get the pre-remainder device translation. float tx = localToDevice.rc(0,3), ty = localToDevice.rc(1,3); skvx::float4 invT = skvx::float4{m11, -m10, -m01, m00} * skvx::float4{tx,tx,ty,ty}; return (invT.xy() + invT.zw()) / det; } CoverageMaskRenderStep::CoverageMaskRenderStep() : RenderStep("CoverageMaskRenderStep", "", // The mask will have AA outsets baked in, but the original bounds for clipping // still require the outset for analytic coverage. Flags::kPerformsShading | Flags::kHasTextures | Flags::kEmitsCoverage | Flags::kOutsetBoundsForAA, /*uniforms=*/{{"maskToDeviceRemainder", SkSLType::kFloat3x3}}, PrimitiveType::kTriangleStrip, kDirectDepthGreaterPass, /*vertexAttrs=*/{}, /*instanceAttrs=*/ // Draw bounds and mask bounds are in normalized relative to the mask texture, // but 'drawBounds' is stored in float since the coords may map outside of // [0,1] for inverse-filled masks. 'drawBounds' is relative to the logical mask // entry's origin, while 'maskBoundsIn' is atlas-relative. Inverse fills swap // the order in 'maskBoundsIn' to be RBLT. {{"drawBounds", VertexAttribType::kFloat4 , SkSLType::kFloat4}, // ltrb {"maskBoundsIn", VertexAttribType::kUShort4_norm, SkSLType::kFloat4}, // Remaining translation extracted from actual 'maskToDevice' transform. {"deviceOrigin", VertexAttribType::kFloat2, SkSLType::kFloat2}, {"depth" , VertexAttribType::kFloat, SkSLType::kFloat}, {"ssboIndices", VertexAttribType::kUInt2, SkSLType::kUInt2}, // deviceToLocal matrix for producing local coords for shader evaluation {"mat0", VertexAttribType::kFloat3, SkSLType::kFloat3}, {"mat1", VertexAttribType::kFloat3, SkSLType::kFloat3}, {"mat2", VertexAttribType::kFloat3, SkSLType::kFloat3}}, /*varyings=*/ {// `maskBounds` are the atlas-relative, sorted bounds of the coverage mask. // `textureCoords` are the atlas-relative UV coordinates of the draw, which // can spill beyond `maskBounds` for inverse fills. // TODO: maskBounds is constant for all fragments for a given instance, // could we store them in the draw's SSBO? {"maskBounds" , SkSLType::kFloat4}, {"textureCoords", SkSLType::kFloat2}, // 'invert' is set to 0 use unmodified coverage, and set to 1 for "1-c". {"invert", SkSLType::kHalf}}) {} std::string CoverageMaskRenderStep::vertexSkSL() const { // Returns the body of a vertex function, which must define a float4 devPosition variable and // must write to an already-defined float2 stepLocalCoords variable. return "float4 devPosition = coverage_mask_vertex_fn(" "float2(sk_VertexID >> 1, sk_VertexID & 1), " "maskToDeviceRemainder, drawBounds, maskBoundsIn, deviceOrigin, " "depth, float3x3(mat0, mat1, mat2), " "maskBounds, textureCoords, invert, stepLocalCoords);\n"; } std::string CoverageMaskRenderStep::texturesAndSamplersSkSL( const ResourceBindingRequirements& bindingReqs, int* nextBindingIndex) const { return EmitSamplerLayout(bindingReqs, nextBindingIndex) + " sampler2D pathAtlas;"; } const char* CoverageMaskRenderStep::fragmentCoverageSkSL() const { return R"( half c = sample(pathAtlas, clamp(textureCoords, maskBounds.LT, maskBounds.RB)).r; outputCoverage = half4(mix(c, 1 - c, invert)); )"; } void CoverageMaskRenderStep::writeVertices(DrawWriter* dw, const DrawParams& params, skvx::uint2 ssboIndices) const { const CoverageMaskShape& coverageMask = params.geometry().coverageMaskShape(); const TextureProxy* proxy = coverageMask.textureProxy(); SkASSERT(proxy); // A quad is a 4-vertex instance. The coordinates are derived from the vertex IDs. DrawWriter::Instances instances(*dw, {}, {}, 4); // The device origin is the translation extracted from the mask-to-device matrix so // that the remaining matrix uniform has less variance between draws. const auto& maskToDevice = params.transform().matrix(); skvx::float2 deviceOrigin = get_device_translation(maskToDevice); // Relative to mask space (device origin and mask-to-device remainder must be applied in shader) skvx::float4 maskBounds = coverageMask.bounds().ltrb(); skvx::float4 drawBounds; if (coverageMask.inverted()) { // Only mask filters trigger complex transforms, and they are never inverse filled. Since // we know this is an inverted mask, then we can exactly map the draw's clip bounds to mask // space so that the clip is still fully covered without branching in the vertex shader. SkASSERT(maskToDevice == SkM44::Translate(deviceOrigin.x(), deviceOrigin.y())); drawBounds = params.clip().drawBounds().makeOffset(-deviceOrigin).ltrb(); // If the mask is fully clipped out, then the shape's mask info should be (0,0,0,0). // If it's not fully clipped out, then the mask info should be non-empty. SkASSERT(!params.clip().transformedShapeBounds().isEmptyNegativeOrNaN() ^ all(maskBounds == 0.f)); if (params.clip().transformedShapeBounds().isEmptyNegativeOrNaN()) { // The inversion check is strict inequality, so (0,0,0,0) would not be detected. Adjust // to (0,0,1/2,1/2) to restrict sampling to the top-left quarter of the top-left pixel, // which should have a value of 0 regardless of filtering mode. maskBounds = skvx::float4{0.f, 0.f, 0.5f, 0.5f}; } else { // Add 1/2px outset to the mask bounds so that clamped coordinates sample the texel // center of the padding around the atlas entry. maskBounds += skvx::float4{-0.5f, -0.5f, 0.5f, 0.5f}; } // and store RBLT so that the 'maskBoundsIn' attribute has xy > zw to detect inverse fill. maskBounds = skvx::shuffle<2,3,0,1>(maskBounds); } else { // If we aren't inverted, then the originally assigned values don't need to be adjusted, but // also ensure the mask isn't empty (otherwise the draw should have been skipped earlier). SkASSERT(!coverageMask.bounds().isEmptyNegativeOrNaN()); SkASSERT(all(maskBounds.xy() < maskBounds.zw())); // Since the mask bounds and draw bounds are 1-to-1 with each other, the clamping of texture // coords is mostly a formality. We inset the mask bounds by 1/2px so that we clamp to the // texel center of the outer row/column of the mask. This should be a no-op for nearest // sampling but prevents any linear sampling from incorporating adjacent data; for atlases // this would just be 0 but for non-atlas coverage masks that might not have padding this // avoids filtering unknown values in an approx-fit texture. drawBounds = maskBounds; maskBounds -= skvx::float4{-0.5f, -0.5f, 0.5f, 0.5f}; } // Move 'drawBounds' and 'maskBounds' into the atlas coordinate space, then adjust the // device translation to undo the atlas origin automatically in the vertex shader. skvx::float2 textureOrigin = skvx::cast(coverageMask.textureOrigin()); maskBounds += textureOrigin.xyxy(); drawBounds += textureOrigin.xyxy(); deviceOrigin -= textureOrigin; // Normalize drawBounds and maskBounds after possibly correcting drawBounds for inverse fills. // The maskToDevice matrix uniform will handle de-normalizing drawBounds for vertex positions. auto atlasSizeInv = skvx::float2{1.f / proxy->dimensions().width(), 1.f / proxy->dimensions().height()}; drawBounds *= atlasSizeInv.xyxy(); maskBounds *= atlasSizeInv.xyxy(); deviceOrigin *= atlasSizeInv; // Since the mask bounds define normalized texels of the texture, we can encode them as // ushort_norm without losing precision to save space. SkASSERT(all((maskBounds >= 0.f) & (maskBounds <= 1.f))); maskBounds = 65535.f * maskBounds + 0.5f; const SkM44& m = coverageMask.deviceToLocal(); instances.append(1) << drawBounds << skvx::cast(maskBounds) << deviceOrigin << params.order().depthAsFloat() << ssboIndices << m.rc(0,0) << m.rc(1,0) << m.rc(3,0) // mat0 << m.rc(0,1) << m.rc(1,1) << m.rc(3,1) // mat1 << m.rc(0,3) << m.rc(1,3) << m.rc(3,3); // mat2 } void CoverageMaskRenderStep::writeUniformsAndTextures(const DrawParams& params, PipelineDataGatherer* gatherer) const { SkDEBUGCODE(UniformExpectationsValidator uev(gatherer, this->uniforms());) const CoverageMaskShape& coverageMask = params.geometry().coverageMaskShape(); const TextureProxy* proxy = coverageMask.textureProxy(); SkASSERT(proxy); // Most coverage masks are aligned with the device pixels, so the params' transform is an // integer translation matrix. This translation is extracted as an instance attribute so that // the remaining transform has a much lower frequency of changing (only complex-transformed // mask filters). skvx::float2 deviceOrigin = get_device_translation(params.transform().matrix()); SkMatrix maskToDevice = params.transform().matrix().asM33(); maskToDevice.preTranslate(-deviceOrigin.x(), -deviceOrigin.y()); // The mask coordinates in the vertex shader will be normalized, so scale by the proxy size // to get back to Skia's texel-based coords. maskToDevice.preScale(proxy->dimensions().width(), proxy->dimensions().height()); // Write uniforms: gatherer->write(maskToDevice); // Write textures and samplers: const bool pixelAligned = params.transform().type() <= Transform::Type::kSimpleRectStaysRect && params.transform().maxScaleFactor() == 1.f && all(deviceOrigin == floor(deviceOrigin + SK_ScalarNearlyZero)); gatherer->add(sk_ref_sp(proxy), {pixelAligned ? SkFilterMode::kNearest : SkFilterMode::kLinear, SkTileMode::kClamp}); } } // namespace skgpu::graphite