| /* | |
| * 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/gpu/graphite/Context.h" | |
| #include "include/gpu/graphite/Recorder.h" | |
| #include "src/gpu/graphite/sparse_strips/AlphaAtlasManager.h" | |
| #include "src/gpu/graphite/sparse_strips/SparseStripsConfig.h" | |
| #include "tests/CtsEnforcement.h" | |
| #include "tests/Test.h" | |
| namespace skgpu::graphite { | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(SparseStrips_AtlasDimensionsAndStride, | |
| reporter, | |
| context, | |
| CtsEnforcement::kToBeDetermined) { | |
| auto recorder = context->makeRecorder(); | |
| AlphaAtlasManager atlasManager(recorder.get()); | |
| // Initial allocation (e.g. 512 bytes = 8 tiles of 8x8) | |
| REPORTER_ASSERT(reporter, atlasManager.requestAlphaSpace(512) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, alphaIndex == 0); | |
| REPORTER_ASSERT(reporter, texPage == 0); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 1); | |
| // Initial page dimensions: 8192 x 1 | |
| sk_sp<TextureProxy> proxy = atlasManager.getPageProxy(0); | |
| REPORTER_ASSERT(reporter, proxy != nullptr); | |
| REPORTER_ASSERT(reporter, proxy->dimensions().width() == SparseStripConfig::kAtlasWidth); | |
| REPORTER_ASSERT(reporter, proxy->dimensions().height() == 1); | |
| // Verify row byte stride alignment (32,768 bytes/row = 8192 pixels * 4 bytes/pixel) | |
| int32_t strideBytes = SparseStripConfig::kAtlasWidthBytes; | |
| REPORTER_ASSERT(reporter, strideBytes == 32768); | |
| // Subsequent allocation on same row/page | |
| REPORTER_ASSERT(reporter, atlasManager.requestAlphaSpace(1024) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, alphaIndex == 512); | |
| REPORTER_ASSERT(reporter, texPage == 0); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 1); | |
| } | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(SparseStrips_AtlasDynamicGrowthAndDoubling, | |
| reporter, | |
| context, | |
| CtsEnforcement::kToBeDetermined) { | |
| auto recorder = context->makeRecorder(); | |
| AlphaAtlasManager atlasManager(recorder.get()); | |
| // Page 0: allocate exactly 1 row (32,768 bytes) | |
| REPORTER_ASSERT(reporter, | |
| atlasManager.requestAlphaSpace(SparseStripConfig::kAtlasWidthBytes) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 0); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(0)->dimensions().height() == 1); | |
| // Page 1: allocating 1 more tile exceeds Page 0, triggers doubling -> 2 rows (65,536 bytes) | |
| REPORTER_ASSERT(reporter, atlasManager.requestAlphaSpace(64) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, alphaIndex == 0); // Atomic on new page, starts at 0 | |
| REPORTER_ASSERT(reporter, texPage == 1); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 2); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(1)->dimensions().height() == 2); | |
| } | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(SparseStrips_AtlasNullBufferAndNullCaps, | |
| reporter, | |
| context, | |
| CtsEnforcement::kToBeDetermined) { | |
| auto recorder = context->makeRecorder(); | |
| AlphaAtlasManager atlasManager(recorder.get()); | |
| // Page 0: 1 row | |
| REPORTER_ASSERT(reporter, | |
| atlasManager.requestAlphaSpace(SparseStripConfig::kAtlasWidthBytes) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 0); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 1); | |
| // Page 1: 2 rows | |
| REPORTER_ASSERT( | |
| reporter, | |
| atlasManager.requestAlphaSpace(SparseStripConfig::kAtlasWidthBytes * 2) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 1); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 2); | |
| // Verify EndCaps recording of NullCaps | |
| EndCaps ends; | |
| ends.addCap(0, 0, 16, 0, 0); | |
| REPORTER_ASSERT(reporter, !ends.hasNullCaps()); | |
| REPORTER_ASSERT(reporter, ends.firstNullCapIndex() == -1); | |
| ends.addCap(16, 0, 16, 0, 1); | |
| REPORTER_ASSERT(reporter, !ends.hasNullCaps()); | |
| REPORTER_ASSERT(reporter, ends.firstNullCapIndex() == -1); | |
| // Exceeding Page 1: NullCaps handling allows MakeStrips to run to completion! | |
| // Transitions to the 3rd nullbuffer (kNullSlot). | |
| uint8_t* nullPtr = atlasManager.requestAlphaSpace(64); | |
| REPORTER_ASSERT(reporter, nullPtr != nullptr); | |
| nullPtr[0] = 0xAB; | |
| auto alloc3 = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc3.has_value()); | |
| REPORTER_ASSERT(reporter, alloc3->second == AlphaAtlasManager::kNullSlot); | |
| REPORTER_ASSERT(reporter, alloc3->first == 0); | |
| REPORTER_ASSERT(reporter, atlasManager.hasNullBuffer()); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(alloc3->second) == nullptr); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageData(alloc3->second)[0] == 0xAB); | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 2); | |
| if (alloc3->second == AlphaAtlasManager::kNullSlot) { | |
| ends.markFirstNullCap(); | |
| } | |
| ends.addCap(32, 0, 16, alloc3->first, alloc3->second); | |
| REPORTER_ASSERT(reporter, ends.hasNullCaps()); | |
| REPORTER_ASSERT(reporter, ends.firstNullCapIndex() == 2); | |
| // Subsequent nullcap does not overwrite firstNullCapIndex | |
| uint8_t* nullPtr2 = atlasManager.requestAlphaSpace(32); | |
| REPORTER_ASSERT(reporter, nullPtr2 != nullptr); | |
| auto alloc4 = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc4.has_value()); | |
| if (alloc4->second == AlphaAtlasManager::kNullSlot) { | |
| ends.markFirstNullCap(); | |
| } | |
| ends.addCap(48, 0, 16, alloc4->first, alloc4->second); | |
| REPORTER_ASSERT(reporter, ends.hasNullCaps()); | |
| REPORTER_ASSERT(reporter, ends.firstNullCapIndex() == 2); | |
| // Flush: recordUploads retires older slot 0 | |
| atlasManager.recordUploads(nullptr); | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 1); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(0) == nullptr); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(1) != nullptr); | |
| REPORTER_ASSERT(reporter, atlasManager.hasNullBuffer()); | |
| // Resolve nullcaps: migrates nullbuffer data to freed slot 0 and updates endcaps | |
| bool resolved = atlasManager.resolveNullCaps(&ends); | |
| REPORTER_ASSERT(reporter, resolved); | |
| REPORTER_ASSERT(reporter, !ends.hasNullCaps()); | |
| REPORTER_ASSERT(reporter, ends.firstNullCapIndex() == -1); | |
| REPORTER_ASSERT(reporter, !atlasManager.hasNullBuffer()); | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 2); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(0) != nullptr); | |
| // Endcaps 2 and 3 now point to slot 0 and have valid proxies | |
| REPORTER_ASSERT(reporter, ends.caps()[2].fTexPage == 0); | |
| REPORTER_ASSERT(reporter, ends.caps()[3].fTexPage == 0); | |
| REPORTER_ASSERT(reporter, ends.proxies()[0] == atlasManager.getPageProxy(0)); | |
| REPORTER_ASSERT(reporter, ends.proxies()[1] == atlasManager.getPageProxy(1)); | |
| REPORTER_ASSERT(reporter, ends.drawStartIndex() == 2); | |
| REPORTER_ASSERT(reporter, ends.drawEndIndex() == 4); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageData(0)[0] == 0xAB); | |
| } | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(SparseStrips_AtlasStraddleToNullBuffer, | |
| reporter, | |
| context, | |
| CtsEnforcement::kNever) { | |
| auto recorder = context->makeRecorder(); | |
| AlphaAtlasManager atlasManager(recorder.get()); | |
| // Fill Page 0 | |
| REPORTER_ASSERT(reporter, | |
| atlasManager.requestAlphaSpace(SparseStripConfig::kAtlasWidthBytes) != nullptr); | |
| auto alloc0 = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc0.has_value()); | |
| // Fill Page 1 almost full, leaving 64 bytes | |
| int32_t page1Capacity = SparseStripConfig::kAtlasWidthBytes * 2; | |
| REPORTER_ASSERT(reporter, atlasManager.requestAlphaSpace(page1Capacity - 64) != nullptr); | |
| auto alloc1 = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc1.has_value()); | |
| REPORTER_ASSERT(reporter, alloc1->second == 1); | |
| // Now start a run that straddles past Page 1's capacity: | |
| // Chunk 1: 64 bytes (fills Page 1) | |
| uint8_t* p1 = atlasManager.requestAlphaSpace(64); | |
| REPORTER_ASSERT(reporter, p1 != nullptr); | |
| for (int i = 0; i < 64; ++i) { | |
| p1[i] = static_cast<uint8_t>(i + 1); | |
| } | |
| // Chunk 2: 64 bytes (overruns Page 1) | |
| uint8_t* p2 = atlasManager.requestAlphaSpace(64); | |
| REPORTER_ASSERT(reporter, p2 != nullptr); | |
| for (int i = 0; i < 64; ++i) { | |
| p2[i] = static_cast<uint8_t>(i + 65); | |
| } | |
| // Finalize run: should memmove the entire 128 bytes to kNullSlot! | |
| auto straddleAlloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, straddleAlloc.has_value()); | |
| REPORTER_ASSERT(reporter, straddleAlloc->second == AlphaAtlasManager::kNullSlot); | |
| REPORTER_ASSERT(reporter, straddleAlloc->first == 0); | |
| REPORTER_ASSERT(reporter, atlasManager.activeSlot() == AlphaAtlasManager::kNullSlot); | |
| REPORTER_ASSERT(reporter, atlasManager.hasNullBuffer()); | |
| const uint8_t* nullData = atlasManager.getPageData(AlphaAtlasManager::kNullSlot); | |
| REPORTER_ASSERT(reporter, nullData != nullptr); | |
| for (int i = 0; i < 128; ++i) { | |
| REPORTER_ASSERT(reporter, nullData[i] == static_cast<uint8_t>(i + 1)); | |
| } | |
| } | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(SparseStrips_AtlasEndCapAtomicity, | |
| reporter, | |
| context, | |
| CtsEnforcement::kToBeDetermined) { | |
| auto recorder = context->makeRecorder(); | |
| AlphaAtlasManager atlasManager(recorder.get()); | |
| // Allocate almost all of Page 0, leaving 64 bytes | |
| REPORTER_ASSERT( | |
| reporter, | |
| atlasManager.requestAlphaSpace(SparseStripConfig::kAtlasWidthBytes - 64) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 0); | |
| REPORTER_ASSERT(reporter, alphaIndex == 0); | |
| } | |
| // Request 128 bytes (which cannot fit in the remaining 64 bytes of Page 0). | |
| // The allocation must be atomic to Page 1: entire 128 bytes at offset 0 of Page 1. | |
| REPORTER_ASSERT(reporter, atlasManager.requestAlphaSpace(128) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 1); | |
| REPORTER_ASSERT(reporter, alphaIndex == 0); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 2); | |
| } | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(SparseStrips_AtlasFlipFlopAtFlush, | |
| reporter, | |
| context, | |
| CtsEnforcement::kToBeDetermined) { | |
| auto recorder = context->makeRecorder(); | |
| AlphaAtlasManager atlasManager(recorder.get()); | |
| // Slot 0: 1 row (32,768 bytes) | |
| REPORTER_ASSERT(reporter, | |
| atlasManager.requestAlphaSpace(SparseStripConfig::kAtlasWidthBytes) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 0); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.activeSlot() == 0); | |
| sk_sp<TextureProxy> initialSlot0 = atlasManager.getPageProxy(0); | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 1); | |
| // Slot 1: 2 rows (65,536 bytes) via XOR flip-flop (0 ^ 1 = 1) | |
| REPORTER_ASSERT(reporter, atlasManager.requestAlphaSpace(64) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 1); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.activeSlot() == 1); | |
| sk_sp<TextureProxy> initialSlot1 = atlasManager.getPageProxy(1); | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 2); | |
| // Flush: recordUploads(nullptr) retires older slot 0 (1 ^ 1 = 0). | |
| // Slot 1 stays right in place in slot 1 (no moving needed)! | |
| atlasManager.recordUploads(nullptr); | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 1); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(0) == nullptr); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(1) == initialSlot1); | |
| REPORTER_ASSERT(reporter, atlasManager.activeSlot() == 1); | |
| // Continue allocating in current active slot (Slot 1): | |
| REPORTER_ASSERT(reporter, atlasManager.requestAlphaSpace(64) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 1); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 1); | |
| // Fill Slot 1 and trigger flip-flop to Slot 0 (1 ^ 1 = 0) -> doubled to 4 rows! | |
| REPORTER_ASSERT( | |
| reporter, | |
| atlasManager.requestAlphaSpace(SparseStripConfig::kAtlasWidthBytes * 2) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 0); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.activeSlot() == 0); | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 2); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(0)->dimensions().height() == 4); | |
| // Second flush: retires older slot 1 (0 ^ 1 = 1). Slot 0 remains active! | |
| atlasManager.recordUploads(nullptr); | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 1); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(1) == nullptr); | |
| REPORTER_ASSERT(reporter, atlasManager.getPageProxy(0)->dimensions().height() == 4); | |
| REPORTER_ASSERT(reporter, atlasManager.activeSlot() == 0); | |
| } | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(SparseStrips_AtlasStraddleAndMemmove, | |
| reporter, | |
| context, | |
| CtsEnforcement::kToBeDetermined) { | |
| auto recorder = context->makeRecorder(); | |
| AlphaAtlasManager atlasManager(recorder.get()); | |
| // Page 0 has 1 row (32,768 bytes). | |
| // Fill Page 0 up to 64 bytes before the end. | |
| int32_t almostFull = SparseStripConfig::kAtlasWidthBytes - 64; | |
| REPORTER_ASSERT(reporter, atlasManager.requestAlphaSpace(almostFull) != nullptr); | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 0); | |
| REPORTER_ASSERT(reporter, alphaIndex == 0); | |
| } | |
| // Now start a run of 128 bytes (two 64-byte chunks). | |
| // Chunk 1 (64 bytes): fills the rest of Page 0. | |
| uint8_t* ptr1 = atlasManager.requestAlphaSpace(64); | |
| REPORTER_ASSERT(reporter, ptr1 != nullptr); | |
| for (int i = 0; i < 64; ++i) { | |
| ptr1[i] = static_cast<uint8_t>(i + 1); | |
| } | |
| // Chunk 2 (64 bytes): straddles past Page 0's capacity into overrun! | |
| uint8_t* ptr2 = atlasManager.requestAlphaSpace(64); | |
| REPORTER_ASSERT(reporter, ptr2 != nullptr); | |
| for (int i = 0; i < 64; ++i) { | |
| ptr2[i] = static_cast<uint8_t>(i + 65); | |
| } | |
| // Finalize the run: should detect straddle, create Page 1, and copy 128 bytes to offset 0 of | |
| // Page 1! | |
| { | |
| auto alloc = atlasManager.finalizeRun(); | |
| REPORTER_ASSERT(reporter, alloc.has_value()); | |
| auto [alphaIndex, texPage] = *alloc; | |
| REPORTER_ASSERT(reporter, texPage == 1); | |
| REPORTER_ASSERT(reporter, alphaIndex == 0); | |
| } | |
| REPORTER_ASSERT(reporter, atlasManager.numPages() == 2); | |
| REPORTER_ASSERT(reporter, atlasManager.activeSlot() == 1); | |
| // Verify that the written bytes were accurately moved into Page 1 at offset 0 | |
| sk_sp<TextureProxy> proxy1 = atlasManager.getPageProxy(1); | |
| REPORTER_ASSERT(reporter, proxy1 != nullptr); | |
| REPORTER_ASSERT(reporter, proxy1->dimensions().height() == 2); // doubled rows | |
| const uint8_t* page1Data = atlasManager.getPageData(1); | |
| REPORTER_ASSERT(reporter, page1Data != nullptr); | |
| for (int i = 0; i < 128; ++i) { | |
| REPORTER_ASSERT(reporter, page1Data[i] == static_cast<uint8_t>(i + 1)); | |
| } | |
| } | |
| } // namespace skgpu::graphite |