| /* | |
| * 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 "tests/Test.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkPoint.h" | |
| #include "include/core/SkTileMode.h" | |
| #include "include/effects/SkGradient.h" | |
| #include "include/gpu/graphite/Context.h" | |
| #include "include/gpu/graphite/Recorder.h" | |
| #include "src/gpu/graphite/Buffer.h" | |
| #include "src/gpu/graphite/BufferManager.h" | |
| #include "src/gpu/graphite/Caps.h" | |
| #include "src/gpu/graphite/DrawContext.h" | |
| #include "src/gpu/graphite/RecorderPriv.h" | |
| #include "src/gpu/graphite/StorageContext.h" | |
| #include "src/gpu/graphite/TextureProxy.h" | |
| #include "src/shaders/gradients/SkGradientBaseShader.h" | |
| #include "src/shaders/gradients/SkLinearGradient.h" | |
| #include <vector> | |
| namespace skgpu::graphite { | |
| namespace { | |
| sk_sp<DrawContext> make_draw_context(Recorder* recorder) { | |
| const Caps* caps = recorder->priv().caps(); | |
| SkColorInfo colorInfo{kRGBA_8888_SkColorType, kPremul_SkAlphaType, nullptr}; | |
| auto texInfo = caps->getDefaultSampledTextureInfo(colorInfo.colorType(), | |
| Mipmapped::kNo, | |
| Protected::kNo, | |
| Renderable::kYes); | |
| sk_sp<TextureProxy> target = TextureProxy::Make(caps, | |
| recorder->priv().resourceProvider(), | |
| {16, 16}, | |
| texInfo, | |
| Budgeted::kYes, | |
| "StorageContextTestTarget"); | |
| return DrawContext::Make(caps, std::move(target), {16, 16}, colorInfo, {}, | |
| /*allowUnpremul=*/false); | |
| } | |
| } // namespace | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextAlignmentTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| SkPoint pts[2] = {{0, 0}, {100, 100}}; | |
| SkColor4f colors[2] = {SkColors::kRed, SkColors::kBlue}; | |
| auto grad1 = | |
| sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kClamp}, {}}); | |
| auto grad2 = | |
| sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kRepeat}, {}}); | |
| const bool useStorage = recorder->priv().caps()->storageBufferSupport(); | |
| StorageContext ctxStorage( | |
| recorder->priv().caps()->resourceBindingRequirements().fMaxFallbackTextureSize, | |
| useStorage); | |
| StorageContext* ctxHandle = &ctxStorage; | |
| const int kFloatCount = useStorage ? 10 : 12; | |
| // 1. Allocate gradient data for shader1 | |
| auto [ptr1, offset1] = ctxHandle->allocateGradientData(2, grad1.get()); | |
| REPORTER_ASSERT(reporter, ptr1 != nullptr); | |
| REPORTER_ASSERT(reporter, offset1 == 0); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| ptr1[i] = 10.f + i; | |
| } | |
| // 2. Allocate gradient data again for shader1 (deduplication check) | |
| auto [ptr1Dup, offset1Dup] = ctxHandle->allocateGradientData(2, grad1.get()); | |
| REPORTER_ASSERT(reporter, ptr1Dup == nullptr); | |
| REPORTER_ASSERT(reporter, offset1Dup == offset1); | |
| // 3. Allocate gradient data for shader2 | |
| auto [ptr2, offset2] = ctxHandle->allocateGradientData(2, grad2.get()); | |
| REPORTER_ASSERT(reporter, ptr2 != nullptr); | |
| REPORTER_ASSERT(reporter, offset2 == kFloatCount); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| ptr2[i] = 30.f + i; | |
| } | |
| // Record vertex requirement with stride 16 and align 16, setting running LCM to 16 | |
| ctxHandle->recordAlignment(/*stride=*/16, /*align=*/16); | |
| ctxHandle->finalizePrecachedStorageData(); | |
| // Finalize storage buffer allocation and check 16-byte alignment | |
| sk_sp<DrawContext> drawContext = make_draw_context(recorder.get()); | |
| auto storageResult = ctxHandle->finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, storageResult.has_value()); | |
| if (std::holds_alternative<BindBufferInfo>(*storageResult)) { | |
| auto bindInfo = std::get<BindBufferInfo>(*storageResult); | |
| REPORTER_ASSERT(reporter, bindInfo.fBuffer != nullptr); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize == 2 * kFloatCount * sizeof(float)); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize % 16 == 0); | |
| if (!recorder->priv().caps()->drawBufferCanBeMapped()) { | |
| return; | |
| } | |
| const char* bufferData = | |
| static_cast<const char*>(const_cast<Buffer*>(bindInfo.fBuffer)->map()) + | |
| bindInfo.fOffset; | |
| const float* floatData = reinterpret_cast<const float*>(bufferData); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| REPORTER_ASSERT(reporter, floatData[i] == 10.f + i); | |
| REPORTER_ASSERT(reporter, floatData[kFloatCount + i] == 30.f + i); | |
| } | |
| } else { | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*storageResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| } | |
| } | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextPaddingAlignmentTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| SkPoint pts[2] = {{0, 0}, {100, 100}}; | |
| SkColor4f colors[2] = {SkColors::kRed, SkColors::kBlue}; | |
| auto grad = sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kClamp}, {}}); | |
| const bool useStorage = recorder->priv().caps()->storageBufferSupport(); | |
| StorageContext ctxStorage( | |
| recorder->priv().caps()->resourceBindingRequirements().fMaxFallbackTextureSize, | |
| useStorage); | |
| StorageContext* ctxHandle = &ctxStorage; | |
| const int kFloatCount = useStorage ? 10 : 12; | |
| const int kBytes = kFloatCount * sizeof(float); | |
| // Allocate gradient data for 1 shader with 2 stops | |
| auto [ptr, offset] = ctxHandle->allocateGradientData(2, grad.get()); | |
| REPORTER_ASSERT(reporter, ptr != nullptr); | |
| REPORTER_ASSERT(reporter, offset == 0); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| ptr[i] = 30.f + i; | |
| } | |
| // Record vertex requirement with stride 32 and align 16, setting running LCM to 32 | |
| ctxHandle->recordAlignment(/*stride=*/32, /*align=*/16); | |
| ctxHandle->finalizePrecachedStorageData(); | |
| // Finalize: gradient bytes should be padded to 64 bytes (aligned to 32 bytes) | |
| sk_sp<DrawContext> drawContext = make_draw_context(recorder.get()); | |
| auto storageResult = ctxHandle->finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, storageResult.has_value()); | |
| if (std::holds_alternative<BindBufferInfo>(*storageResult)) { | |
| auto bindInfo = std::get<BindBufferInfo>(*storageResult); | |
| REPORTER_ASSERT(reporter, bindInfo.fBuffer != nullptr); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize == 64); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize % 32 == 0); | |
| if (!recorder->priv().caps()->drawBufferCanBeMapped()) { | |
| return; | |
| } | |
| const char* bufferData = | |
| static_cast<const char*>(const_cast<Buffer*>(bindInfo.fBuffer)->map()) + | |
| bindInfo.fOffset; | |
| const float* floatData = reinterpret_cast<const float*>(bufferData); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| REPORTER_ASSERT(reporter, floatData[i] == 30.f + i); | |
| } | |
| for (int i = kBytes; i < 64; ++i) { | |
| REPORTER_ASSERT(reporter, bufferData[i] == 0); | |
| } | |
| } else { | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*storageResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| } | |
| } | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextAppendVertexTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| SkPoint pts[2] = {{0, 0}, {100, 100}}; | |
| SkColor4f colors[2] = {SkColors::kRed, SkColors::kBlue}; | |
| auto grad = sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kClamp}, {}}); | |
| const bool useStorage = recorder->priv().caps()->storageBufferSupport(); | |
| StorageContext ctxStorage( | |
| recorder->priv().caps()->resourceBindingRequirements().fMaxFallbackTextureSize, | |
| useStorage); | |
| StorageContext* ctxHandle = &ctxStorage; | |
| const int kFloatCount = useStorage ? 10 : 12; | |
| const int kBytes = kFloatCount * sizeof(float); | |
| // 1. Allocate gradient data | |
| auto [gradPtr, gradOffset] = ctxHandle->allocateGradientData(2, grad.get()); | |
| REPORTER_ASSERT(reporter, gradPtr != nullptr); | |
| REPORTER_ASSERT(reporter, gradOffset == 0); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| gradPtr[i] = 40.f + i; | |
| } | |
| // 2. Record vertex alignment requirement: stride 24, align 16 -> running LCM = 48 | |
| ctxHandle->recordAlignment(/*stride=*/24, /*align=*/16); | |
| // 3. Finalize precached storage data: aligned to running LCM (48 bytes) | |
| ctxHandle->finalizePrecachedStorageData(); | |
| // 4. Append vertices with stride 24, align 16, count 2 | |
| // With storage: stride 24, align 16 -> LCM = 48. Padded grad size = 48. | |
| // Without storage: stride 24 -> 32, align 16 -> LCM = 32. Padded grad size = 64. | |
| float verts[12] = {0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, 10.f, 11.f}; | |
| uint32_t vOffset = ctxHandle->appendVertices(verts, /*count=*/2, /*stride=*/24, /*align=*/16); | |
| const uint32_t expectedVOffset = useStorage ? 48 : 64; | |
| REPORTER_ASSERT(reporter, vOffset == expectedVOffset); | |
| // 5. Finalize storage buffer | |
| sk_sp<DrawContext> drawContext = make_draw_context(recorder.get()); | |
| auto storageResult = ctxHandle->finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, storageResult.has_value()); | |
| if (std::holds_alternative<BindBufferInfo>(*storageResult)) { | |
| auto bindInfo = std::get<BindBufferInfo>(*storageResult); | |
| REPORTER_ASSERT(reporter, bindInfo.fBuffer != nullptr); | |
| // Total size = 48 (aligned gradient) + 48 (vertices) = 96 bytes | |
| REPORTER_ASSERT(reporter, bindInfo.fSize == 96); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize % 48 == 0); | |
| if (!recorder->priv().caps()->drawBufferCanBeMapped()) { | |
| return; | |
| } | |
| const char* bufferData = | |
| static_cast<const char*>(const_cast<Buffer*>(bindInfo.fBuffer)->map()) + | |
| bindInfo.fOffset; | |
| const float* floatData = reinterpret_cast<const float*>(bufferData); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| REPORTER_ASSERT(reporter, floatData[i] == 40.f + i); | |
| } | |
| for (int i = kBytes; i < 48; ++i) { | |
| REPORTER_ASSERT(reporter, bufferData[i] == 0); | |
| } | |
| REPORTER_ASSERT(reporter, memcmp(bufferData + 48, verts, sizeof(verts)) == 0); | |
| } else { | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*storageResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| } | |
| } | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextMultipleRenderStepsTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| SkPoint pts[2] = {{0, 0}, {100, 100}}; | |
| SkColor4f colors[2] = {SkColors::kRed, SkColors::kBlue}; | |
| auto grad = sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kClamp}, {}}); | |
| const bool useStorage = recorder->priv().caps()->storageBufferSupport(); | |
| StorageContext ctxStorage( | |
| recorder->priv().caps()->resourceBindingRequirements().fMaxFallbackTextureSize, | |
| useStorage); | |
| StorageContext* ctxHandle = &ctxStorage; | |
| const int kFloatCount = useStorage ? 10 : 12; | |
| const int kBytes = kFloatCount * sizeof(float); | |
| // 1. Allocate gradient data | |
| auto [gradPtr, gradOffset] = ctxHandle->allocateGradientData(2, grad.get()); | |
| REPORTER_ASSERT(reporter, gradPtr != nullptr); | |
| REPORTER_ASSERT(reporter, gradOffset == 0); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| gradPtr[i] = 50.f + i; | |
| } | |
| // 2. Record alignments from multiple render steps: | |
| // Step A: stride 24, align 16 -> LCM = 48 | |
| // Step B: stride 32, align 16 -> LCM = 32 | |
| // Running LCM = LCM(48, 32) = 96 | |
| ctxHandle->recordAlignment(/*stride=*/24, /*align=*/16); | |
| ctxHandle->recordAlignment(/*stride=*/32, /*align=*/16); | |
| // 3. Finalize precached storage data: aligned to running LCM (96 bytes) | |
| ctxHandle->finalizePrecachedStorageData(); | |
| // 4. Step A appends 1 vertex of 24 bytes (stride 24, align 16) | |
| float dataA[6] = {1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; | |
| uint32_t offsetA = ctxHandle->appendVertices(dataA, /*count=*/1, /*stride=*/24, /*align=*/16); | |
| const uint32_t expectedOffsetA = useStorage ? 96 : 64; | |
| REPORTER_ASSERT(reporter, offsetA == expectedOffsetA); | |
| // 5. Step B appends 1 vertex of 32 bytes (stride 32, align 16) | |
| // With storage: Local vertex size is 24; next 32-byte aligned offset is 32 (8 bytes | |
| // zero-padding) Without storage: Step A is already padded to 32 bytes; next aligned offset | |
| // is 32. | |
| float dataB[8] = {1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f}; | |
| uint32_t offsetB = ctxHandle->appendVertices(dataB, /*count=*/1, /*stride=*/32, /*align=*/16); | |
| const uint32_t expectedOffsetB = useStorage ? 128 : 96; | |
| REPORTER_ASSERT(reporter, offsetB == expectedOffsetB); | |
| // 6. Finalize storage buffer: Total size = 96 (gradient) + 32 (local offset) + 32 (dataB) = 160 | |
| sk_sp<DrawContext> drawContext = make_draw_context(recorder.get()); | |
| auto storageResult = ctxHandle->finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, storageResult.has_value()); | |
| if (std::holds_alternative<BindBufferInfo>(*storageResult)) { | |
| auto bindInfo = std::get<BindBufferInfo>(*storageResult); | |
| REPORTER_ASSERT(reporter, bindInfo.fBuffer != nullptr); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize == 160); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize % 32 == 0); | |
| if (!recorder->priv().caps()->drawBufferCanBeMapped()) { | |
| return; | |
| } | |
| const char* bufferData = | |
| static_cast<const char*>(const_cast<Buffer*>(bindInfo.fBuffer)->map()) + | |
| bindInfo.fOffset; | |
| const float* floatData = reinterpret_cast<const float*>(bufferData); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| REPORTER_ASSERT(reporter, floatData[i] == 50.f + i); | |
| } | |
| for (int i = kBytes; i < 96; ++i) { | |
| REPORTER_ASSERT(reporter, bufferData[i] == 0); | |
| } | |
| REPORTER_ASSERT(reporter, memcmp(bufferData + 96, dataA, sizeof(dataA)) == 0); | |
| for (int i = 120; i < 128; ++i) { | |
| REPORTER_ASSERT(reporter, bufferData[i] == 0); | |
| } | |
| REPORTER_ASSERT(reporter, memcmp(bufferData + 128, dataB, sizeof(dataB)) == 0); | |
| } else { | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*storageResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| } | |
| } | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextLCMVariantsTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| struct TestCase { | |
| size_t stride; | |
| size_t align; | |
| uint32_t expectedLCM; | |
| }; | |
| const TestCase testCases[] = { | |
| {/*stride=*/12, /*align=*/16, /*expectedLCM=*/48}, | |
| {/*stride=*/20, /*align=*/16, /*expectedLCM=*/80}, | |
| {/*stride=*/24, /*align=*/16, /*expectedLCM=*/48}, | |
| {/*stride=*/28, /*align=*/16, /*expectedLCM=*/112}, | |
| {/*stride=*/36, /*align=*/16, /*expectedLCM=*/144}, | |
| {/*stride=*/40, /*align=*/16, /*expectedLCM=*/80}, | |
| {/*stride=*/64, /*align=*/16, /*expectedLCM=*/64}, | |
| }; | |
| SkPoint pts[2] = {{0, 0}, {100, 100}}; | |
| SkColor4f colors[2] = {SkColors::kRed, SkColors::kBlue}; | |
| auto grad = sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kClamp}, {}}); | |
| const bool useStorage = recorder->priv().caps()->storageBufferSupport(); | |
| const int kFloatCount = useStorage ? 10 : 12; | |
| const size_t kBytes = kFloatCount * sizeof(float); | |
| for (const auto& tc : testCases) { | |
| StorageContext ctx( | |
| recorder->priv().caps()->resourceBindingRequirements().fMaxFallbackTextureSize, | |
| useStorage); | |
| // Allocate 2 gradient stops | |
| auto [gradPtr, gradOffset] = ctx.allocateGradientData(2, grad.get()); | |
| REPORTER_ASSERT(reporter, gradPtr != nullptr); | |
| REPORTER_ASSERT(reporter, gradOffset == 0); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| gradPtr[i] = 60.f + i; | |
| } | |
| ctx.recordAlignment(tc.stride, tc.align); | |
| ctx.finalizePrecachedStorageData(); | |
| size_t paddedStride = tc.stride; | |
| size_t paddedAlign = tc.align; | |
| uint32_t expectedLCM = tc.expectedLCM; | |
| if (!useStorage) { | |
| paddedStride = SkAlignTo<size_t>(tc.stride, 16); | |
| paddedAlign = std::max<size_t>(tc.align, 16); | |
| expectedLCM = BufferAligner::LcmAlignment(SkTo<uint32_t>(paddedAlign), | |
| SkTo<uint32_t>(paddedStride)); | |
| } | |
| uint32_t expectedPaddedGradSize = | |
| SkAlignNonPow2(static_cast<uint32_t>(kBytes), expectedLCM); | |
| // Append 2 vertices | |
| std::vector<char> vert(tc.stride * 2, 0); | |
| for (size_t i = 0; i < vert.size(); ++i) { | |
| vert[i] = static_cast<char>((i + 1) & 0x7F); | |
| } | |
| uint32_t vOffset = ctx.appendVertices(vert.data(), /*count=*/2, tc.stride, tc.align); | |
| REPORTER_ASSERT(reporter, vOffset == expectedPaddedGradSize); | |
| REPORTER_ASSERT(reporter, vOffset % paddedAlign == 0); | |
| REPORTER_ASSERT(reporter, vOffset % expectedLCM == 0); | |
| sk_sp<DrawContext> drawContext = make_draw_context(recorder.get()); | |
| auto storageResult = ctx.finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, storageResult.has_value()); | |
| if (std::holds_alternative<BindBufferInfo>(*storageResult)) { | |
| auto bindInfo = std::get<BindBufferInfo>(*storageResult); | |
| REPORTER_ASSERT(reporter, bindInfo.fBuffer != nullptr); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize == expectedPaddedGradSize + tc.stride * 2); | |
| if (!recorder->priv().caps()->drawBufferCanBeMapped()) { | |
| continue; | |
| } | |
| const char* bufferData = | |
| static_cast<const char*>(const_cast<Buffer*>(bindInfo.fBuffer)->map()) + | |
| bindInfo.fOffset; | |
| const float* floatData = reinterpret_cast<const float*>(bufferData); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| REPORTER_ASSERT(reporter, floatData[i] == 60.f + i); | |
| } | |
| for (size_t i = kBytes; i < expectedPaddedGradSize; ++i) { | |
| REPORTER_ASSERT(reporter, bufferData[i] == 0); | |
| } | |
| REPORTER_ASSERT( | |
| reporter, | |
| memcmp(bufferData + expectedPaddedGradSize, vert.data(), vert.size()) == 0); | |
| } else { | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*storageResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| } | |
| } | |
| } | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextVertexOnlyAndResetTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| const bool useStorage = recorder->priv().caps()->storageBufferSupport(); | |
| StorageContext ctx( | |
| recorder->priv().caps()->resourceBindingRequirements().fMaxFallbackTextureSize, | |
| useStorage); | |
| REPORTER_ASSERT(reporter, ctx.isEmpty()); | |
| sk_sp<DrawContext> drawContext = make_draw_context(recorder.get()); | |
| // 1. Finalize on empty context returns nullopt | |
| ctx.finalizePrecachedStorageData(); | |
| auto emptyResult = ctx.finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, !emptyResult.has_value()); | |
| // 2. Vertex-only allocation without gradients | |
| ctx.recordAlignment(/*stride=*/24, /*align=*/16); | |
| ctx.finalizePrecachedStorageData(); | |
| float vert[6] = {1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; | |
| uint32_t offset = ctx.appendVertices(vert, /*count=*/1, /*stride=*/24, /*align=*/16); | |
| REPORTER_ASSERT(reporter, offset == 0); | |
| REPORTER_ASSERT(reporter, !ctx.isEmpty()); | |
| auto vertexResult = ctx.finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, vertexResult.has_value()); | |
| if (std::holds_alternative<BindBufferInfo>(*vertexResult)) { | |
| auto bindInfo = std::get<BindBufferInfo>(*vertexResult); | |
| REPORTER_ASSERT(reporter, bindInfo.fBuffer != nullptr); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize == 24); | |
| if (recorder->priv().caps()->drawBufferCanBeMapped()) { | |
| const char* bufferData = | |
| static_cast<const char*>(const_cast<Buffer*>(bindInfo.fBuffer)->map()) + | |
| bindInfo.fOffset; | |
| REPORTER_ASSERT(reporter, memcmp(bufferData, vert, sizeof(vert)) == 0); | |
| } | |
| } else { | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*vertexResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| } | |
| // 3. Reset cache and verify clean state | |
| ctx.resetCache(); | |
| REPORTER_ASSERT(reporter, ctx.isEmpty()); | |
| // 4. Subsequent allocation after reset starts at offset 0 | |
| SkPoint pts[2] = {{0, 0}, {100, 100}}; | |
| SkColor4f colors[2] = {SkColors::kRed, SkColors::kBlue}; | |
| auto grad = sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kClamp}, {}}); | |
| const int kFloatCount = useStorage ? 10 : 12; | |
| auto [gradPtr, gradOffset] = ctx.allocateGradientData(2, grad.get()); | |
| REPORTER_ASSERT(reporter, gradPtr != nullptr); | |
| REPORTER_ASSERT(reporter, gradOffset == 0); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| gradPtr[i] = 70.f + i; | |
| } | |
| ctx.finalizePrecachedStorageData(); | |
| auto postResetResult = ctx.finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, postResetResult.has_value()); | |
| if (std::holds_alternative<BindBufferInfo>(*postResetResult)) { | |
| auto resetInfo = std::get<BindBufferInfo>(*postResetResult); | |
| REPORTER_ASSERT(reporter, resetInfo.fBuffer != nullptr); | |
| if (!recorder->priv().caps()->drawBufferCanBeMapped()) { | |
| return; | |
| } | |
| const char* resetBufferData = | |
| static_cast<const char*>(const_cast<Buffer*>(resetInfo.fBuffer)->map()) + | |
| resetInfo.fOffset; | |
| const float* resetFloats = reinterpret_cast<const float*>(resetBufferData); | |
| for (int i = 0; i < kFloatCount; ++i) { | |
| REPORTER_ASSERT(reporter, resetFloats[i] == 70.f + i); | |
| } | |
| } else { | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*postResetResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| } | |
| } | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextMultiStopGradientTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| const bool useStorage = recorder->priv().caps()->storageBufferSupport(); | |
| StorageContext ctx( | |
| recorder->priv().caps()->resourceBindingRequirements().fMaxFallbackTextureSize, | |
| useStorage); | |
| SkPoint pts[2] = {{0, 0}, {100, 100}}; | |
| const int alignedOffsets9 = useStorage ? 9 : SkAlign4(9); | |
| const int count9 = useStorage ? (9 * 5) : SkAlign4(alignedOffsets9 + 9 * 4); | |
| std::vector<SkColor4f> colors9(9, SkColors::kRed); | |
| std::vector<SkScalar> pos9 = {0.f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 1.0f}; | |
| auto grad9 = sk_make_sp<SkLinearGradient>( | |
| pts, SkGradient{SkGradient::Colors{colors9, pos9, SkTileMode::kClamp}, {}}); | |
| auto [ptr9, offset9] = ctx.allocateGradientData(9, grad9.get()); | |
| REPORTER_ASSERT(reporter, ptr9 != nullptr); | |
| REPORTER_ASSERT(reporter, offset9 == 0); | |
| // Populate offsets and colors | |
| for (int i = 0; i < 9; ++i) { | |
| ptr9[i] = pos9[i]; | |
| } | |
| for (int i = 9; i < alignedOffsets9; ++i) { | |
| ptr9[i] = 0.f; // padding | |
| } | |
| for (int i = 0; i < 9; ++i) { | |
| ptr9[alignedOffsets9 + i * 4 + 0] = static_cast<float>(i); | |
| ptr9[alignedOffsets9 + i * 4 + 1] = static_cast<float>(i) * 0.1f; | |
| ptr9[alignedOffsets9 + i * 4 + 2] = 0.5f; | |
| ptr9[alignedOffsets9 + i * 4 + 3] = 1.0f; | |
| } | |
| const int alignedOffsets17 = useStorage ? 17 : SkAlign4(17); | |
| const int count17 = useStorage ? (17 * 5) : SkAlign4(alignedOffsets17 + 17 * 4); | |
| std::vector<SkColor4f> colors17(17, SkColors::kBlue); | |
| std::vector<SkScalar> pos17(17); | |
| for (int i = 0; i < 17; ++i) { | |
| pos17[i] = static_cast<float>(i) / 16.0f; | |
| } | |
| auto grad17 = sk_make_sp<SkLinearGradient>( | |
| pts, SkGradient{SkGradient::Colors{colors17, pos17, SkTileMode::kRepeat}, {}}); | |
| auto [ptr17, offset17] = ctx.allocateGradientData(17, grad17.get()); | |
| REPORTER_ASSERT(reporter, ptr17 != nullptr); | |
| REPORTER_ASSERT(reporter, offset17 == count9); | |
| for (int i = 0; i < 17; ++i) { | |
| ptr17[i] = pos17[i]; | |
| } | |
| for (int i = 17; i < alignedOffsets17; ++i) { | |
| ptr17[i] = 0.f; // padding | |
| } | |
| for (int i = 0; i < 17; ++i) { | |
| ptr17[alignedOffsets17 + i * 4 + 0] = static_cast<float>(i); | |
| ptr17[alignedOffsets17 + i * 4 + 1] = static_cast<float>(i) * 0.05f; | |
| ptr17[alignedOffsets17 + i * 4 + 2] = 0.25f; | |
| ptr17[alignedOffsets17 + i * 4 + 3] = 1.0f; | |
| } | |
| ctx.finalizePrecachedStorageData(); | |
| sk_sp<DrawContext> drawContext = make_draw_context(recorder.get()); | |
| auto storageResult = ctx.finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, storageResult.has_value()); | |
| if (std::holds_alternative<BindBufferInfo>(*storageResult)) { | |
| auto bindInfo = std::get<BindBufferInfo>(*storageResult); | |
| REPORTER_ASSERT(reporter, bindInfo.fBuffer != nullptr); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize == (count9 + count17) * sizeof(float)); | |
| REPORTER_ASSERT(reporter, bindInfo.fSize % 16 == 0 || useStorage); | |
| if (!recorder->priv().caps()->drawBufferCanBeMapped()) { | |
| return; | |
| } | |
| const char* bufferData = | |
| static_cast<const char*>(const_cast<Buffer*>(bindInfo.fBuffer)->map()) + | |
| bindInfo.fOffset; | |
| const float* floatData = reinterpret_cast<const float*>(bufferData); | |
| // Check grad9 data | |
| for (int i = 0; i < 9; ++i) { | |
| REPORTER_ASSERT(reporter, floatData[i] == pos9[i]); | |
| } | |
| for (int i = 0; i < 9; ++i) { | |
| REPORTER_ASSERT(reporter, | |
| floatData[alignedOffsets9 + i * 4 + 0] == static_cast<float>(i)); | |
| REPORTER_ASSERT(reporter, | |
| floatData[alignedOffsets9 + i * 4 + 1] == static_cast<float>(i) * 0.1f); | |
| } | |
| // Check grad17 data | |
| const float* float17 = floatData + count9; | |
| for (int i = 0; i < 17; ++i) { | |
| REPORTER_ASSERT(reporter, float17[i] == pos17[i]); | |
| } | |
| for (int i = 0; i < 17; ++i) { | |
| REPORTER_ASSERT(reporter, | |
| float17[alignedOffsets17 + i * 4 + 0] == static_cast<float>(i)); | |
| REPORTER_ASSERT(reporter, | |
| float17[alignedOffsets17 + i * 4 + 1] == static_cast<float>(i) * 0.05f); | |
| } | |
| } else { | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*storageResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| } | |
| } | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextFallbackStridedCopyTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| StorageContext ctx( | |
| recorder->priv().caps()->resourceBindingRequirements().fMaxFallbackTextureSize, | |
| /*storageBufferSupport=*/false); | |
| // Record alignment for stride 24, align 16. Because storageBufferSupport is false, | |
| // stride is rounded up to 32 and align to at least 16. LCM = 32. | |
| ctx.recordAlignment(/*stride=*/24, /*align=*/16); | |
| ctx.finalizePrecachedStorageData(); | |
| // Append 2 vertices with original stride 24 (6 floats = 24 bytes per vertex; 48 bytes total) | |
| float verts[12] = { | |
| 1.f, | |
| 2.f, | |
| 3.f, | |
| 4.f, | |
| 5.f, | |
| 6.f, | |
| 7.f, | |
| 8.f, | |
| 9.f, | |
| 10.f, | |
| 11.f, | |
| 12.f, | |
| }; | |
| uint32_t offset = ctx.appendVertices(verts, /*count=*/2, /*stride=*/24, /*align=*/16); | |
| REPORTER_ASSERT(reporter, offset == 0); | |
| sk_sp<DrawContext> drawContext = make_draw_context(recorder.get()); | |
| REPORTER_ASSERT(reporter, drawContext != nullptr); | |
| // Finalize: padded to 32 bytes per vertex -> 2 * 32 = 64 bytes total | |
| auto storageResult = ctx.finalize(recorder.get(), drawContext.get()); | |
| REPORTER_ASSERT(reporter, storageResult.has_value()); | |
| REPORTER_ASSERT(reporter, std::holds_alternative<sk_sp<TextureProxy>>(*storageResult)); | |
| auto proxy = std::get<sk_sp<TextureProxy>>(*storageResult); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| REPORTER_ASSERT(reporter, proxy->dimensions() == SkISize::Make(4, 1)); | |
| REPORTER_ASSERT(reporter, drawContext->snapDrawTask() != nullptr); | |
| } | |
| DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(StorageContextCapacityLimitsTest, | |
| reporter, | |
| context, | |
| CtsEnforcement::kApiLevel_202404) { | |
| std::unique_ptr<Recorder> recorder = context->makeRecorder(); | |
| SkPoint pts[2] = {{0, 0}, {100, 100}}; | |
| SkColor4f colors[2] = {SkColors::kRed, SkColors::kBlue}; | |
| auto grad1 = | |
| sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kClamp}, {}}); | |
| auto grad2 = | |
| sk_make_sp<SkLinearGradient>(pts, SkGradient{{colors, {}, SkTileMode::kRepeat}, {}}); | |
| // 1. Exceeding kMaxGradientStops (1M stops) fails gracefully | |
| { | |
| StorageContext ctx(recorder->priv().caps()->resourceBindingRequirements(), | |
| /*storageBufferSupport=*/true); | |
| auto [ptr, offset] = ctx.allocateGradientData(1024 * 1024 + 1, grad1.get()); | |
| REPORTER_ASSERT(reporter, ptr == nullptr); | |
| REPORTER_ASSERT(reporter, offset == -1); | |
| REPORTER_ASSERT(reporter, ctx.isEmpty()); | |
| } | |
| // 2. Exceeding fallback texture capacity (maxFallbackTextureSize = 2 -> 4 texels = 64 bytes) | |
| // fails gracefully for both gradient allocation and vertex appending. | |
| { | |
| StorageContext ctx(/*maxFallbackTextureSize=*/2, /*storageBufferSupport=*/false); | |
| #if defined(GPU_TEST_UTILS) | |
| REPORTER_ASSERT(reporter, ctx.maxStorageBytes() == 64); | |
| #endif | |
| // A 2-stop gradient takes 12 floats = 48 bytes, which fits in 64 bytes | |
| auto [ptr1, offset1] = ctx.allocateGradientData(2, grad1.get()); | |
| REPORTER_ASSERT(reporter, ptr1 != nullptr); | |
| REPORTER_ASSERT(reporter, offset1 == 0); | |
| // A second 2-stop gradient would require 96 bytes > 64 bytes, so it fails gracefully | |
| auto [ptr2, offset2] = ctx.allocateGradientData(2, grad2.get()); | |
| REPORTER_ASSERT(reporter, ptr2 == nullptr); | |
| REPORTER_ASSERT(reporter, offset2 == -1); | |
| ctx.recordAlignment(/*stride=*/16, /*align=*/16); | |
| ctx.finalizePrecachedStorageData(); | |
| // Appending 1 vertex of 16 bytes fits (48 + 16 = 64 bytes) | |
| float vert[4] = {1.f, 2.f, 3.f, 4.f}; | |
| uint32_t vOffset1 = ctx.appendVertices(vert, /*count=*/1, /*stride=*/16, /*align=*/16); | |
| REPORTER_ASSERT(reporter, vOffset1 == 48); | |
| // Appending another vertex of 16 bytes exceeds 64 bytes and returns 0 without growing | |
| uint32_t vOffset2 = ctx.appendVertices(vert, /*count=*/1, /*stride=*/16, /*align=*/16); | |
| REPORTER_ASSERT(reporter, vOffset2 == 0); | |
| SkDEBUGCODE(REPORTER_ASSERT(reporter, ctx.vertexSize() == 16);) | |
| } | |
| } | |
| } // namespace skgpu::graphite | |