fix: secure Symbol Retained drawable lifetime and record investigations
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-12 23:27:06 +09:00
parent badb515d59
commit 343ca2fa68
9 changed files with 1043 additions and 45 deletions

View File

@@ -10,6 +10,22 @@
#include <mln/renderer/buckets/symbol_bucket.hpp>
#include <mln/renderer/symbol_shader_provenance.hpp>
#include <mln/shaders/symbol_layer_ubo.hpp>
#include <mln/gfx/drawable_atlases_tweaker.hpp>
#include <mln/gfx/drawable_impl.hpp>
#include <mln/gfx/symbol_drawable_data.hpp>
#include <mln/geometry/line_atlas.hpp>
#include <mln/renderer/image_manager.hpp>
#include <mln/renderer/layers/render_symbol_layer.hpp>
#include <mln/renderer/layers/symbol_layer_tweaker.hpp>
#include <mln/renderer/paint_parameters.hpp>
#include <mln/renderer/pattern_atlas.hpp>
#include <mln/renderer/render_source.hpp>
#include <mln/renderer/render_tile.hpp>
#include <mln/renderer/render_tree.hpp>
#include <mln/renderer/tile_render_data.hpp>
#include <mln/style/layers/symbol_layer.hpp>
#include <mln/style/sources/geojson_source_impl.hpp>
#include <mln/tile/tile.hpp>
#include <gtest/gtest.h>
@@ -22,6 +38,40 @@
using namespace mln;
namespace mln {
// Exercise the same internal methods used by prepare/update/result collection,
// without a GPU Context or a second copy of their compatibility predicates.
struct SymbolRetainedLifetimeTestAccess {
static void prepare(RenderSymbolLayer& layer, const LayerPrepareParameters& params) { layer.prepare(params); }
static void setTiles(RenderSymbolLayer& layer, Immutable<std::vector<RenderTile>> owner, RenderTiles refs) {
layer.setRenderTiles(std::move(owner), std::move(refs));
}
static void setup(RenderSymbolLayer& layer, std::shared_ptr<TileLayerGroup> group) {
layer.layerGroup = std::move(group);
layer.passes = RenderPass::Translucent;
layer.layerTweaker = std::make_shared<SymbolLayerTweaker>(layer.getID(), layer.evaluatedProperties);
layer.isRenderable = true;
}
static const LayerTweakerPtr& tweaker(const RenderSymbolLayer& layer) { return layer.layerTweaker; }
static bool bind(RenderSymbolLayer& layer, gfx::Drawable& drawable, const RenderTile& tile) {
return layer.bindRenderTile(drawable, tile);
}
static bool reuse(RenderSymbolLayer& layer, const RenderTile& tile, const std::shared_ptr<Bucket>& bucket,
const std::shared_ptr<TileAtlasTextures>& atlases) {
return layer.rebindTileDrawables(tile, bucket, atlases);
}
static SymbolRenderResult atlasResult(const RenderSymbolLayer& layer, const gfx::Drawable& drawable) {
SymbolRenderResult result;
layer.appendRetainedTileResult(result, drawable);
return result;
}
static void removeLayer(RenderSymbolLayer& layer) {
UniqueChangeRequestVec changes;
layer.layerRemoved(changes);
}
};
} // namespace mln
namespace {
RetainedUnderlayKey key(Size size = {640, 480}, int32_t boundary = 4) {
RetainedUnderlayKey result{size, 0, boundary, {}};
@@ -2109,3 +2159,425 @@ TEST(FastGateTrace, RetainedSamplesHaveDistinctPhasesAndExplicitLimit) {
EXPECT_EQ(frame.post.retained[0].cache.build, 1u);
EXPECT_EQ(frame.post.retained[0].cache.sourceRevision, 7u);
}
namespace {
class LifetimeUniformBuffers final : public gfx::UniformBufferArray {
public:
void bind(gfx::RenderPass&) override {}
protected:
std::unique_ptr<gfx::UniformBuffer> copy(const gfx::UniformBuffer&) override { return nullptr; }
};
class LifetimeDrawable final : public gfx::Drawable {
public:
LifetimeDrawable() : Drawable("symbol-lifetime") {}
void draw(PaintParameters&) const override {}
void updateVertexAttributes(gfx::VertexAttributeArrayPtr, std::size_t, gfx::DrawMode,
gfx::IndexVectorBasePtr, const SegmentBase*, std::size_t) override {}
void setVertices(std::vector<uint8_t>&&, std::size_t, gfx::AttributeDataType) override {}
void setIndexData(gfx::IndexVectorBasePtr, std::vector<UniqueDrawSegment>) override {}
const gfx::UniformBufferArray& getUniformBuffers() const override { return buffers; }
gfx::UniformBufferArray& mutableUniformBuffers() override { return buffers; }
private:
LifetimeUniformBuffers buffers;
};
class LifetimeGroup final : public TileLayerGroup {
public:
LifetimeGroup() : TileLayerGroup(0, 4, "symbol-lifetime") {}
const gfx::UniformBufferArray& getUniformBuffers() const override { return buffers; }
gfx::UniformBufferArray& mutableUniformBuffers() override { return buffers; }
private:
LifetimeUniformBuffers buffers;
};
class LifetimeTexture final : public gfx::Texture2D {
public:
Texture2D& setSamplerConfiguration(const SamplerState&) noexcept override { return *this; }
Texture2D& setFormat(gfx::TexturePixelType, gfx::TextureChannelDataType) noexcept override { return *this; }
Texture2D& setSize(Size value) noexcept override { size = value; return *this; }
Texture2D& setImage(std::shared_ptr<PremultipliedImage>) noexcept override { dirty = true; return *this; }
gfx::TexturePixelType getFormat() const noexcept override { return gfx::TexturePixelType::RGBA; }
Size getSize() const noexcept override { return size; }
size_t getDataSize() const noexcept override { return size.width * size.height * 4; }
size_t getPixelStride() const noexcept override { return 4; }
size_t numChannels() const noexcept override { return 4; }
void create() override {}
void upload(const void*, const Size&) override { dirty = false; }
void uploadSubRegion(const void*, const Size&, uint16_t, uint16_t) override { dirty = true; }
void upload() override { dirty = false; }
bool needsUpload() const noexcept override { return dirty; }
bool dirty = false;
private:
Size size{8, 8};
};
class LifetimeTileData final : public TileRenderData {
public:
LifetimeTileData(std::shared_ptr<TileAtlasTextures> atlases, std::shared_ptr<int> live_)
: TileRenderData(std::move(atlases)), live(std::move(live_)) { ++*live; }
~LifetimeTileData() override { --*live; }
private:
// This fixture supplies atlas-only render data. The base implementations
// assert: prepare() and the empty-group collector must dispatch here.
const LayerRenderData* getLayerRenderData(const style::Layer::Impl&) const override { return nullptr; }
Bucket* getBucket(const style::Layer::Impl&) const override { return nullptr; }
std::shared_ptr<int> live;
};
class LifetimeTile final : public Tile {
public:
LifetimeTile(std::shared_ptr<int> liveData_, std::shared_ptr<int> liveTiles_)
: Tile(Kind::Geometry, OverscaledTileID{2, 0, {2, 1, 1}}, "symbol-lifetime-source"),
liveData(std::move(liveData_)), liveTiles(std::move(liveTiles_)) { ++*liveTiles; }
~LifetimeTile() override { --*liveTiles; }
std::unique_ptr<TileRenderData> createRenderData() override {
return std::make_unique<LifetimeTileData>(atlases, liveData);
}
void cancel() override {}
bool layerPropertiesUpdated(const Immutable<style::LayerProperties>&) override { return true; }
bool holdForFade() const override { return true; }
std::shared_ptr<TileAtlasTextures> atlases = std::make_shared<TileAtlasTextures>();
private:
std::shared_ptr<int> liveData, liveTiles;
};
class LifetimeSource final : public RenderSource {
public:
LifetimeSource(std::shared_ptr<int> liveData, std::shared_ptr<int> liveTiles)
: RenderSource(makeMutable<style::GeoJSONSource::Impl>(
"symbol-lifetime-source", style::GeoJSONOptions::defaultOptions())),
tile(std::make_unique<LifetimeTile>(std::move(liveData), std::move(liveTiles))) {}
bool isLoaded() const override { return true; }
void update(Immutable<style::Source::Impl>, const std::vector<Immutable<style::LayerProperties>>&,
bool, bool, const TileParameters&) override {}
std::unique_ptr<RenderItem> createRenderItem() override { return nullptr; }
void prepare(const SourcePrepareParameters& params) override {
auto tiles = makeMutable<std::vector<RenderTile>>();
tiles->emplace_back(tile->id.toUnwrapped(), *tile);
tiles->back().prepare(params);
owner = std::move(tiles);
sorted = std::make_shared<std::vector<std::reference_wrapper<const RenderTile>>>(
std::initializer_list<std::reference_wrapper<const RenderTile>>{owner->front()});
}
Immutable<std::vector<RenderTile>> getRawRenderTiles() const override { return owner; }
RenderTiles getRenderTilesSortedByYPosition() const override { return sorted; }
// Deliberately excludes the fading tile, unlike the Symbol path.
RenderTiles getRenderTiles() const override {
return std::make_shared<std::vector<std::reference_wrapper<const RenderTile>>>();
}
void updateFadingTiles() override {}
bool hasFadingTiles() const override { return true; }
std::unordered_map<std::string, std::vector<Feature>> queryRenderedFeatures(
const ScreenLineString&, const TransformState&, const std::unordered_map<std::string, const RenderLayer*>&,
const RenderedQueryOptions&, const mat4&) const override { return {}; }
std::vector<Feature> querySourceFeatures(const SourceQueryOptions&) const override { return {}; }
void reduceMemoryUse() override {}
void dumpDebugLogs() const override {}
std::unique_ptr<LifetimeTile> tile;
Immutable<std::vector<RenderTile>> owner = makeMutable<std::vector<RenderTile>>();
RenderTiles sorted;
};
class SymbolRetainedLifetime : public ::testing::Test {
protected:
SymbolRetainedLifetime()
: styleLayer("symbol-lifetime", "symbol-lifetime-source"),
layer(staticImmutableCast<style::SymbolLayer::Impl>(styleLayer.baseImpl)) {
state.setSize({640, 480});
source = std::make_unique<LifetimeSource>(liveData, liveTiles);
source->tile->atlases->glyph = std::make_shared<LifetimeTexture>();
source->tile->atlases->icon = std::make_shared<LifetimeTexture>();
SymbolRetainedLifetimeTestAccess::setup(layer, group);
prepare();
}
void prepare() {
TransformParameters transform(state);
const auto debug = MapDebugOptions::NoDebug;
source->prepare({transform, debug, *images, "symbol-lifetime-source"});
SymbolRetainedLifetimeTestAccess::prepare(layer, {source.get(), *images, patterns, lines, state});
}
std::shared_ptr<SymbolBucket> makeBucket() {
return std::make_shared<SymbolBucket>(makeMutable<style::SymbolLayoutProperties::PossiblyEvaluated>(),
std::map<std::string, Immutable<style::LayerProperties>>{}, 16.0f, 1.0f, 0.0f, false, false,
"symbol-lifetime", std::vector<SymbolInstance>{}, std::vector<SortKeyRange>{}, 1.0f, false,
std::vector<style::TextWritingModeType>{}, false);
}
LifetimeDrawable& add(const std::shared_ptr<Bucket>& bucket, style::SymbolType type = style::SymbolType::Text,
bool halo = false) {
auto drawable = std::make_unique<LifetimeDrawable>();
auto& ref = *drawable;
ref.setTileID(source->tile->id);
ref.setRenderPass(RenderPass::Translucent);
ref.setLayerTweaker(SymbolRetainedLifetimeTestAccess::tweaker(layer));
ref.setBinders(bucket, nullptr); // The production rebind path never reads binders.
ref.setShader(std::make_shared<Step119UnknownSymbolProgram>());
const auto atlases = source->tile->atlases;
auto tweaker = std::make_shared<gfx::DrawableAtlasesTweaker>(atlases,
shaders::idSymbolImageIconTexture, shaders::idSymbolImageTexture, type == style::SymbolType::Text,
type == style::SymbolType::IconSDF, style::AlignmentType::Viewport, false, true);
ref.addTweaker(tweaker);
ref.setType(gfx::SymbolDrawableData::drawableType);
ref.setData(std::make_unique<gfx::SymbolDrawableData>(halo, false, type, style::AlignmentType::Viewport,
style::AlignmentType::Viewport, style::SymbolPlacementType::Point, style::IconTextFitType::None,
false, atlases, tweaker));
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::bind(layer, ref, source->owner->front()));
tweaker->init(ref); // Actual production texture binding, CPU texture backend only.
group->addDrawable(RenderPass::Translucent, source->tile->id, std::move(drawable));
return ref;
}
bool reuse(const std::shared_ptr<Bucket>& bucket) {
return SymbolRetainedLifetimeTestAccess::reuse(layer, source->owner->front(), bucket, source->tile->atlases);
}
void releaseTiles() {
SymbolRetainedLifetimeTestAccess::setTiles(layer, makeMutable<std::vector<RenderTile>>(),
std::make_shared<std::vector<std::reference_wrapper<const RenderTile>>>());
source.reset();
}
std::shared_ptr<int> liveData = std::make_shared<int>(0), liveTiles = std::make_shared<int>(0);
style::SymbolLayer styleLayer;
RenderSymbolLayer layer;
std::shared_ptr<LifetimeGroup> group = std::make_shared<LifetimeGroup>();
TransformState state;
std::shared_ptr<ImageManager> images = ImageManager::create();
PatternAtlas patterns;
LineAtlas lines;
std::unique_ptr<LifetimeSource> source;
};
} // namespace
TEST_F(SymbolRetainedLifetime, ConsecutiveSourcePrepareRebindsAndReleasesOldRenderData) {
const auto bucket = makeBucket();
auto& drawable = add(bucket);
const auto* oldTile = drawable.getRenderTile();
const auto drawableID = drawable.getID();
const auto atlasTweaker = drawable.getTweakers().front();
prepare();
EXPECT_NE(oldTile, &source->owner->front()); // Old owner is still held by drawable.
EXPECT_EQ(*liveData, 2);
EXPECT_TRUE(reuse(bucket));
EXPECT_EQ(drawable.getRenderTile(), &source->owner->front());
EXPECT_EQ(*liveData, 1); // No old tree/owner remains after rebind.
EXPECT_EQ(drawable.getID(), drawableID);
EXPECT_EQ(drawable.getTweakers().front(), atlasTweaker);
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
EXPECT_TRUE(drawable.getRenderTile()->getAtlasTextures()); // Valid new renderData.
}
TEST_F(SymbolRetainedLifetime, MismatchedOwnerAndElementAreRejectedBeforeReading) {
const auto bucket = makeBucket();
auto& drawable = add(bucket);
const auto validOldOwner = source->owner;
const auto oldRefs = source->sorted;
prepare();
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::bind(layer, drawable, validOldOwner->front()));
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
SymbolRetainedLifetimeTestAccess::setTiles(layer, source->owner, oldRefs);
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::bind(layer, drawable, source->owner->front()));
EXPECT_FALSE(layer.retainedRenderResult(1, 0).complete);
// Keep the counterexample owner live; no dangling pointer is dereferenced.
}
TEST_F(SymbolRetainedLifetime, AtlasReplacementDropsWholeTextIconHaloGroupAndBindsNewTextures) {
const auto bucket = makeBucket();
add(bucket);
add(bucket, style::SymbolType::Text, true);
add(bucket, style::SymbolType::IconRGBA);
source->tile->atlases = std::make_shared<TileAtlasTextures>();
source->tile->atlases->glyph = std::make_shared<LifetimeTexture>();
source->tile->atlases->icon = std::make_shared<LifetimeTexture>();
prepare();
EXPECT_FALSE(reuse(bucket));
EXPECT_TRUE(group->empty());
auto& rebuilt = add(bucket);
EXPECT_EQ(rebuilt.getTexture(shaders::idSymbolImageTexture), source->tile->atlases->glyph);
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, rebuilt).complete);
}
TEST_F(SymbolRetainedLifetime, OneIncompatibleMemberDropsWholeGroup) {
for (unsigned mismatch = 0; mismatch < 5; ++mismatch) {
const auto bucket = makeBucket();
add(bucket);
auto& halo = add(bucket, style::SymbolType::Text, true);
add(bucket, style::SymbolType::IconRGBA);
if (mismatch == 0) halo.setBinders(makeBucket(), nullptr);
if (mismatch == 1) halo.setLayerTweaker(nullptr);
if (mismatch == 2) halo.clearTweakers();
if (mismatch == 3) halo.setData(nullptr);
if (mismatch == 4) halo.setType(0);
EXPECT_FALSE(reuse(bucket));
EXPECT_TRUE(group->empty());
}
}
TEST_F(SymbolRetainedLifetime, MissingAtlasDropsOldDrawables) {
const auto bucket = makeBucket();
add(bucket);
source->tile->atlases.reset();
prepare();
EXPECT_FALSE(reuse(bucket));
EXPECT_TRUE(group->empty());
}
TEST_F(SymbolRetainedLifetime, SameAtlasTextureReplacementRejectsOldBindingAndRequiresNewBuild) {
const auto bucket = makeBucket();
auto& drawable = add(bucket);
auto original = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
ASSERT_TRUE(original.complete);
SymbolResultCertificate certificate;
const auto binding = step119Binding();
step119Settle(certificate, original, binding);
const auto atlasTweaker = drawable.getTweakers().front();
source->tile->atlases->glyph = std::make_shared<LifetimeTexture>();
EXPECT_TRUE(reuse(bucket)); // Same atlas policy is retained and will run normally.
auto unbound = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
EXPECT_FALSE(unbound.complete);
EXPECT_FALSE(certificate.matchesCandidate(unbound));
atlasTweaker->init(drawable);
auto rebound = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
EXPECT_TRUE(rebound.complete);
EXPECT_NE(rebound.bytes, original.bytes);
EXPECT_FALSE(certificate.observeFull(rebound, binding, true));
certificate.completeFull(binding, true, false);
EXPECT_TRUE(certificate.observeFull(rebound, binding, true));
EXPECT_FALSE(certificate.candidateHasBuild(binding));
certificate.completeFull(binding, true, true);
EXPECT_TRUE(certificate.observeFull(rebound, binding, true));
certificate.completeFull(binding, true, false, true);
EXPECT_TRUE(certificate.canFast(binding));
EXPECT_EQ(drawable.getTweakers().front(), atlasTweaker);
}
TEST_F(SymbolRetainedLifetime, InPlaceTextureUploadRejectsEvidenceUntilUploaded) {
const auto bucket = makeBucket();
auto& drawable = add(bucket);
const auto texture = std::static_pointer_cast<LifetimeTexture>(source->tile->atlases->glyph);
texture->uploadSubRegion(nullptr, {1, 1}, 0, 0);
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
texture->upload();
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
// Pixel updates AFTER upload additionally require the real content epoch;
// Step119SymbolResults.RealTileGlyphImageStyleContextCallbacksRejectBuiltCertificate covers it.
}
TEST_F(SymbolRetainedLifetime, OwnerIdentityDoesNotEnterCertificateAndEvidenceKeepsRenderData) {
// Empty native Symbol group still exercises the complete production collector.
auto first = layer.retainedRenderResult(1, 0);
ASSERT_TRUE(first.complete);
prepare();
auto second = layer.retainedRenderResult(1, 0);
ASSERT_TRUE(second.complete);
EXPECT_EQ(first.bytes, second.bytes);
EXPECT_EQ(*liveData, 2);
SymbolResultCertificate certificate;
const auto binding = step119Binding();
EXPECT_FALSE(certificate.observeFull(first, binding, true));
certificate.completeFull(binding, true, false);
EXPECT_TRUE(certificate.observeFull(second, binding, true));
certificate.completeFull(binding, true, true);
EXPECT_TRUE(certificate.observeFull(second, binding, true));
certificate.completeFull(binding, true, false, true);
EXPECT_TRUE(certificate.canFast(binding));
first = {}; // Old renderData can now leave, despite the old ref-list having existed.
EXPECT_EQ(*liveData, 1);
second = {};
releaseTiles();
EXPECT_EQ(*liveTiles, 0); // Container evidence does not own Tile&.
EXPECT_EQ(*liveData, 1); // But it owns RenderTile -> TileRenderData.
certificate.invalidate();
EXPECT_EQ(*liveData, 0);
}
TEST_F(SymbolRetainedLifetime, LayerRemovalAndCertificateInvalidationReleaseResources) {
const auto bucket = makeBucket();
auto& drawable = add(bucket);
std::weak_ptr<TileAtlasTextures> atlas = source->tile->atlases;
SymbolResultCertificate certificate;
step119Settle(certificate, SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable), step119Binding());
SymbolRetainedLifetimeTestAccess::removeLayer(layer);
EXPECT_TRUE(group->empty());
releaseTiles();
EXPECT_EQ(*liveTiles, 0);
EXPECT_EQ(*liveData, 0);
EXPECT_FALSE(atlas.expired()); // Retained atlas evidence outlives the removed group.
certificate.invalidate();
EXPECT_TRUE(atlas.expired());
}
TEST_F(SymbolRetainedLifetime, SameSourceIDReplacementRejectsOldAtlasAndReleasesOldContainer) {
const auto bucket = makeBucket();
add(bucket);
std::weak_ptr<TileAtlasTextures> oldAtlas = source->tile->atlases;
source = std::make_unique<LifetimeSource>(liveData, liveTiles); // Same source/tile ID, new Tile and atlas.
source->tile->atlases->glyph = std::make_shared<LifetimeTexture>();
source->tile->atlases->icon = std::make_shared<LifetimeTexture>();
prepare(); // Never invoke a method on the old Tile&, which has already gone.
EXPECT_FALSE(reuse(bucket));
EXPECT_TRUE(group->empty());
EXPECT_TRUE(oldAtlas.expired());
EXPECT_EQ(*liveData, 1);
auto& rebuilt = add(bucket);
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, rebuilt).complete);
}
TEST_F(SymbolRetainedLifetime, TileLeavesCoverCleanupRemovesDrawable) {
const auto bucket = makeBucket();
add(bucket);
releaseTiles();
// The early update branch has no Context dependencies. Use removeAllDrawables,
// the production branch's cleanup entry, instead of fabricating a Context reference.
EXPECT_EQ(layer.removeAllDrawables(), 1u);
EXPECT_TRUE(group->empty());
EXPECT_EQ(*liveData, 0);
EXPECT_EQ(*liveTiles, 0);
}
TEST_F(SymbolRetainedLifetime, TextIconSdfAndHalosKeepIdentityAcrossRepeatedPrepare) {
const auto bucket = makeBucket();
add(bucket);
add(bucket, style::SymbolType::Text, true);
add(bucket, style::SymbolType::IconRGBA);
add(bucket, style::SymbolType::IconSDF);
add(bucket, style::SymbolType::IconSDF, true);
std::vector<util::SimpleIdentity> identities;
std::vector<gfx::DrawableTweakerPtr> tweakers;
std::vector<std::vector<uint8_t>> inputs;
group->visitDrawables([&](gfx::Drawable& drawable) {
identities.push_back(drawable.getID());
tweakers.push_back(drawable.getTweakers().front());
const auto result = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
EXPECT_TRUE(result.complete);
inputs.push_back(result.bytes);
});
for (unsigned generation = 0; generation < 3; ++generation) {
prepare();
ASSERT_TRUE(reuse(bucket));
ASSERT_EQ(group->getDrawableCount(), identities.size());
std::size_t index = 0;
group->visitDrawables([&](gfx::Drawable& drawable) {
EXPECT_EQ(drawable.getID(), identities[index]);
EXPECT_EQ(drawable.getTweakers().front(), tweakers[index]);
EXPECT_EQ(drawable.getRenderTile(), &source->owner->front());
const auto result = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
EXPECT_TRUE(result.complete);
EXPECT_EQ(result.bytes, inputs[index]);
++index;
});
EXPECT_EQ(*liveData, 1);
}
// This checks production binding evidence, not complete GPU scene equivalence.
}
TEST_F(SymbolRetainedLifetime, UnknownSymbolTypeRejectsEvidenceAndDropsWholeGroup) {
const auto bucket = makeBucket();
add(bucket);
auto& drawable = add(bucket, style::SymbolType::Text, true);
const auto atlases = source->tile->atlases;
const auto tweaker = std::static_pointer_cast<gfx::DrawableAtlasesTweaker>(drawable.getTweakers().front());
drawable.setData(std::make_unique<gfx::SymbolDrawableData>(true, false, static_cast<style::SymbolType>(255),
style::AlignmentType::Viewport, style::AlignmentType::Viewport, style::SymbolPlacementType::Point,
style::IconTextFitType::None, false, atlases, tweaker));
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
EXPECT_FALSE(reuse(bucket));
EXPECT_TRUE(group->empty());
}