| /* | |
| * Copyright 2026 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/core/SkPoint.h" | |
| #include "src/core/SkRandom.h" | |
| #include "src/gpu/graphite/sparse_strips/MSAA_LUT.h" | |
| #include "tests/Test.h" | |
| #include <algorithm> | |
| #include <cmath> | |
| #include <cstdint> | |
| #include <cstring> | |
| namespace skgpu::graphite { | |
| namespace { | |
| uint8_t compute_naive_mask(SkPoint p0, SkPoint p1) { | |
| float dx = p1.fX - p0.fX; | |
| float dy = p1.fY - p0.fY; | |
| if (std::abs(dx) < 1e-9f && std::abs(dy) < 1e-9f) return 0; | |
| float nx = dy; | |
| float ny = -dx; | |
| float len = std::hypot(nx, ny); | |
| nx /= len; | |
| ny /= len; | |
| if (nx < 0.0f) { | |
| nx = -nx; | |
| ny = -ny; | |
| } | |
| float c = nx * p0.fX + ny * p0.fY; | |
| SparseStripConfig::SubSampleType mask = 0; | |
| for (int i = 0; i < SparseStripConfig::kNumSubSamples; ++i) { | |
| float sx = (kMsaaPattern<SparseStripConfig::SubSampleType>[i] + 0.5f) / | |
| static_cast<float>(SparseStripConfig::kNumSubSamples); | |
| float sy = (i + 0.5f) / static_cast<float>(SparseStripConfig::kNumSubSamples); | |
| if (nx * sx + ny * sy - c > 0.0f) { | |
| mask |= (1 << i); | |
| } | |
| } | |
| return mask; | |
| } | |
| SparseStripConfig::SubSampleType lookup_LUT(SkPoint p0, SkPoint p1, | |
| const SparseStripConfig::LUTArray& lut) { | |
| float dx = p1.fX - p0.fX; | |
| float dy = p1.fY - p0.fY; | |
| if (std::abs(dx) < 1e-9f && std::abs(dy) < 1e-9f) { | |
| return 0; | |
| } | |
| float nx = dy; | |
| float ny = -dx; | |
| // Match the Naive compute's normal orientation | |
| if (nx < 0.0f) { | |
| nx = -nx; | |
| ny = -ny; | |
| } | |
| float c = nx * p0.fX + ny * p0.fY; | |
| bool isPos = ny <= 0.0f; | |
| float D = nx + std::abs(ny); | |
| // Squash the slope | |
| float s = std::abs(ny) / D; | |
| float t = ((isPos ? nx : D) - c) / D; | |
| static constexpr int kWidth = SparseStripConfig::kLUTMaskWidth; | |
| static constexpr int kHeight = SparseStripConfig::kLUTMaskHeight; | |
| static constexpr int halfHeight = kHeight / 2; | |
| int u = std::clamp(static_cast<int>(std::floor(t * kWidth)), 0, kWidth - 1); | |
| int v_mod = std::clamp(static_cast<int>(std::floor(s * halfHeight)), 0, halfHeight - 1); | |
| int v = isPos ? (v_mod + halfHeight) : v_mod; | |
| return lut[v * kWidth + u]; | |
| } | |
| // Note: Although it is possible for a line segment to end within a pixel, we do not test that case | |
| // here. The LUT's quantization relies solely on a parameterized slope and intercept, effectively | |
| // treating the segment as an infinite line that crosses the entire pixel. Downstream subsample | |
| // masking logic independently handles the specific case of endpoints falling inside a pixel. | |
| static SkPoint pick_random_square_point(SkRandom* rand) { | |
| int edge = rand->nextU() % 4; | |
| float t = rand->nextF(); | |
| switch (edge) { | |
| case 0: | |
| return {t, 0.0f}; | |
| case 1: | |
| return {1.0f, t}; | |
| case 2: | |
| return {t, 1.0f}; | |
| default: | |
| return {0.0f, t}; | |
| } | |
| } | |
| template <typename LookupFunc> | |
| void test_LUT(skiatest::Reporter* reporter, | |
| const char* lutName, | |
| const SparseStripConfig::LUTArray& lut, | |
| uint32_t seed, | |
| LookupFunc lookupFunc) { | |
| SkRandom rand(seed); | |
| constexpr int kIterations = 10000; | |
| // Track and report the number of 1,2, and 3 sample errors. These are not reported as errors as | |
| // some noise is expected due to quantization to the LUT. | |
| constexpr uint32_t kErrorLimit = 3; | |
| std::array<uint32_t, kErrorLimit> minorErrorCount = {0, 0, 0}; | |
| for (int i = 0; i < kIterations; ++i) { | |
| SkPoint p0 = pick_random_square_point(&rand); | |
| SkPoint p1 = pick_random_square_point(&rand); | |
| uint8_t expected = compute_naive_mask(p0, p1); | |
| uint8_t actual = lookupFunc(p0, p1, lut); | |
| uint8_t diff = expected ^ actual; | |
| uint32_t bitErrors = std::popcount(diff); | |
| if (bitErrors > kErrorLimit) { | |
| uint32_t hexP0X, hexP0Y, hexP1X, hexP1Y; | |
| std::memcpy(&hexP0X, &p0.fX, 4); | |
| std::memcpy(&hexP0Y, &p0.fY, 4); | |
| std::memcpy(&hexP1X, &p1.fX, 4); | |
| std::memcpy(&hexP1Y, &p1.fY, 4); | |
| ERRORF(reporter, | |
| "[%s] CRITICAL Fail at iter %d (Seed: %u). Expected 0x%02x, " | |
| "Got 0x%02x (%u bits diff)\n" | |
| "P0: %f, %f (0x%08x, 0x%08x)\n" | |
| "P1: %f, %f (0x%08x, 0x%08x)", | |
| lutName, i, seed, expected, actual, bitErrors, | |
| p0.fX, p0.fY, hexP0X, hexP0Y, p1.fX, p1.fY, hexP1X, hexP1Y); | |
| } else if (bitErrors > 0) { | |
| minorErrorCount[bitErrors - 1]++; | |
| } | |
| } | |
| INFOF(reporter, | |
| "[%s] Minor Error Summary (Seed: %u): 1-bit: %u, 2-bit: %u, 3-bit: %u", | |
| lutName, | |
| seed, | |
| minorErrorCount[0], | |
| minorErrorCount[1], | |
| minorErrorCount[2]); | |
| } | |
| } // namespace | |
| DEF_TEST(SparseStrips_LUTTest, reporter) { | |
| auto now = std::chrono::high_resolution_clock::now(); | |
| uint32_t seed = static_cast<uint32_t>(now.time_since_epoch().count()); | |
| const SparseStripConfig::LUTArray msaaLUT = GenerateMSAALUT(); | |
| test_LUT(reporter, "SlopeBasedLUT", msaaLUT, seed, lookup_LUT); | |
| } | |
| } // namespace skgpu::graphite |