fix: secure Symbol Retained drawable lifetime and record investigations
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user