Can a GeoJsonDocumentRasterOverlay be re-added after removal?

Once a GeoJsonDocumentRasterOverlay is removed from a tileset, attempting to add it again - to the same tileset or a different one - crashes the application. Is this intended? If so, it should be documented. If not, it’s a bug.

``` cpp


#include <CesiumGeospatial/LocalHorizontalCoordinateSystem.h>
#include <CesiumGeospatial/Cartographic.h>
#include <CesiumGeospatial/Ellipsoid.h>
#include <CesiumGeospatial/GlobeTransforms.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <Cesium3DTilesContent/GltfConverters.h>
#include <CesiumRasterOverlays/DebugColorizeTilesRasterOverlay.h>
#include <CesiumRasterOverlays/WebMapServiceRasterOverlay.h>
#include <CesiumRasterOverlays/TileMapServiceRasterOverlay.h>
#include <CesiumRasterOverlays/IonRasterOverlay.h>
#include <CesiumRasterOverlays/QuadtreeRasterOverlayTileProvider.h>
#include <Cesium3DTilesSelection/EllipsoidTilesetLoader.h>
#include <CesiumRasterOverlays/GeoJsonDocumentRasterOverlay.h>
#include <CesiumCurl/CurlAssetAccessor.h>

#include <thread>
#include <filesystem>
#include <fstream>
#include <assert.h>

namespace fs = std::filesystem;

std::vector<std::byte> readFile(const std::filesystem::path &fileName)
{
    std::ifstream file(fileName, std::ios::binary | std::ios::ate);
    assert(file);

    std::streamoff size = file.tellg();
    file.seekg(0, std::ios::beg);

    std::vector<std::byte> buffer{size_t(size)};
    file.read(reinterpret_cast<char *>(buffer.data()), std::streamsize(size));

    return buffer;
}
using namespace CesiumAsync;
using namespace CesiumUtility;
using namespace CesiumRasterOverlays;
using namespace CesiumVectorData;
using namespace CesiumGeospatial;
using namespace Cesium3DTilesSelection;
class SimpleTaskProcessor : public CesiumAsync::ITaskProcessor
{
public:
    virtual void startTask(std::function<void()> f) override
    {
        auto *heapFunc = new std::function<void()>(std::move(f));

        std::thread t([](void *taskData)
                      {
                          std::unique_ptr<std::function<void()>> func(
                              static_cast<std::function<void()> *>(taskData));
                          (*func)(); // auto-deletes when leaving scope
                      },
                      heapFunc);
        t.detach();
    }
};

class SimplePrepareRendererResource
    : public Cesium3DTilesSelection::IPrepareRendererResources
{
public:
    std::atomic<size_t> totalAllocation{};

    struct AllocationResult
    {
        AllocationResult(std::atomic<size_t> &allocCount_)
            : allocCount{allocCount_}
        {
            ++allocCount;
        }

        ~AllocationResult() noexcept { --allocCount; }

        std::atomic<size_t> &allocCount;
    };

    ~SimplePrepareRendererResource() noexcept {}

    virtual CesiumAsync::Future<TileLoadResultAndRenderResources>
    prepareInLoadThread(
        const CesiumAsync::AsyncSystem &asyncSystem,
        TileLoadResult &&tileLoadResult,
        const glm::dmat4 & /*transform*/,
        const std::any & /*rendererOptions*/) override
    {
        prepareInLoadThreadTestCallback(tileLoadResult);
        return asyncSystem.createResolvedFuture(TileLoadResultAndRenderResources{
            std::move(tileLoadResult),
            new AllocationResult{totalAllocation}});
    }

    virtual void *prepareInMainThread(
        Cesium3DTilesSelection::Tile & /*tile*/,
        void *pLoadThreadResult) override
    {
        if (pLoadThreadResult)
        {
            AllocationResult *loadThreadResult =
                reinterpret_cast<AllocationResult *>(pLoadThreadResult);
            delete loadThreadResult;
        }

        return new AllocationResult{totalAllocation};
    }

    virtual void free(
        Cesium3DTilesSelection::Tile & /*tile*/,
        void *pLoadThreadResult,
        void *pMainThreadResult) noexcept override
    {
        if (pMainThreadResult)
        {
            AllocationResult *mainThreadResult =
                reinterpret_cast<AllocationResult *>(pMainThreadResult);
            delete mainThreadResult;
        }

        if (pLoadThreadResult)
        {
            AllocationResult *loadThreadResult =
                reinterpret_cast<AllocationResult *>(pLoadThreadResult);
            delete loadThreadResult;
        }
    }

    virtual void *prepareRasterInLoadThread(
        CesiumGltf::ImageAsset & /*image*/,
        const std::any & /*rendererOptions*/) override
    {
        return new AllocationResult{totalAllocation};
    }

    virtual void *prepareRasterInMainThread(
        CesiumRasterOverlays::RasterOverlayTile & /*rasterTile*/,
        void *pLoadThreadResult) override
    {
        if (pLoadThreadResult)
        {
            AllocationResult *loadThreadResult =
                reinterpret_cast<AllocationResult *>(pLoadThreadResult);
            delete loadThreadResult;
        }

        return new AllocationResult{totalAllocation};
    }

    virtual void freeRaster(
        const CesiumRasterOverlays::RasterOverlayTile & /*rasterTile*/,
        void *pLoadThreadResult,
        void *pMainThreadResult) noexcept override
    {
        if (pMainThreadResult)
        {
            AllocationResult *mainThreadResult =
                reinterpret_cast<AllocationResult *>(pMainThreadResult);
            delete mainThreadResult;
        }

        if (pLoadThreadResult)
        {
            AllocationResult *loadThreadResult =
                reinterpret_cast<AllocationResult *>(pLoadThreadResult);
            delete loadThreadResult;
        }
    }

    virtual void attachRasterInMainThread(
        const Cesium3DTilesSelection::Tile & /*tile*/,
        int32_t /*overlayTextureCoordinateID*/,
        const CesiumRasterOverlays::RasterOverlayTile & /*rasterTile*/,
        void * /*pMainThreadRendererResources*/,
        const glm::dvec2 & /*translation*/,
        const glm::dvec2 & /*scale*/) override {}

    virtual void detachRasterInMainThread(
        const Cesium3DTilesSelection::Tile & /*tile*/,
        int32_t /*overlayTextureCoordinateID*/,
        const CesiumRasterOverlays::RasterOverlayTile & /*rasterTile*/,
        void * /*pMainThreadRendererResources*/) noexcept override {}

    std::function<void(const TileLoadResult &)> prepareInLoadThreadTestCallback =
        [](const TileLoadResult & /*result*/) {};
};

int main(int argc, char **argv)
{
    fs::path exePath = fs::canonical(argv[0]);
    fs::path exeDir = exePath.parent_path();
    fs::current_path(exeDir);

    auto taskProcessor = std::make_shared<SimpleTaskProcessor>();

    auto asyncSystem = std::make_shared<CesiumAsync::AsyncSystem>(taskProcessor);

    auto curlAssetAccessor = std::make_shared<CesiumCurl::CurlAssetAccessor>();

    auto prepareRendererResource = std::make_shared<SimplePrepareRendererResource>();

    auto creditSystem = std::make_shared<CesiumUtility::CreditSystem>();

    Cesium3DTilesSelection::TilesetExternals externals{
        curlAssetAccessor,
        prepareRendererResource,
        *asyncSystem,
        creditSystem};

    Cesium3DTilesSelection::TilesetOptions options{};

    Cesium3DTilesSelection::Tileset *tileset = new Cesium3DTilesSelection::Tileset(externals, "./tileset.json", options);

    GeoJsonDocumentRasterOverlayOptions geoJsonOptions{
        VectorStyle{
            LineStyle{
                ColorStyle{Color{255, 0, 0, 255}, ColorMode::Normal},
                2.0,
                LineWidthMode::Pixels},
            PolygonStyle{ColorStyle{
                             .color = Color(0, 255, 0, 50),
                             .colorMode = ColorMode::Normal},
                         LineStyle{
                             ColorStyle{Color{255, 255, 0, 255}, ColorMode::Normal},
                             2.0,
                             LineWidthMode::Pixels}}},
        Ellipsoid::WGS84,
        0};

    CesiumUtility::Result<GeoJsonDocument> docResult = GeoJsonDocument::fromGeoJson(readFile("sample.geojson"));

    GeoJsonDocument doc = docResult.value.value();

    auto docPtr = std::make_shared<GeoJsonDocument>(doc);

    auto raster = new CesiumRasterOverlays::GeoJsonDocumentRasterOverlay(*asyncSystem, "some overlay",
                                                                         docPtr, geoJsonOptions);

    CesiumUtility::IntrusivePointer<const CesiumRasterOverlays::RasterOverlay> safePtr = raster;

    tileset->getOverlays().add(safePtr);

    tileset->getOverlays().remove(safePtr);

    assert(safePtr->getReferenceCount() > 0);

    // boom
    tileset->getOverlays().add(safePtr);

    tileset->getOverlays().remove(safePtr);
    
    return 0;
}

@orelron98 Do you have any error messages or logs to share about exactly where it’s crashing? And do you know if this crash happens with all raster overlays, or just the GeoJsonDocumentRasterOverlay?

There is an access violation occurring during the second add operation following a remove (see the ‘boom’ comment in the code). The snippet is standalone and should reproduce the crash immediately. I will try to investigate and share the call stack when possible.

LIBASYNC_ASSERT is failing, It looks like createTileProvider is attempting to call .thenInMainThread() on an invalid/empty future.
look at the provided call stack for more details

geo-raster-bug.exe!async::detail::basic_task<std::shared_ptr<CesiumVectorData::GeoJsonDocument>>::then_internal<CesiumAsync::CesiumImpl::ImmediateScheduler<CesiumAsync::CesiumImpl::QueuedScheduler>,`CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider'::`2'::<lambda_1>,async::task<std::shared_ptr<CesiumVectorData::GeoJsonDocument>>>(CesiumAsync::CesiumImpl::ImmediateScheduler<CesiumAsync::CesiumImpl::QueuedScheduler> & sched, CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider::__l2::<lambda_1> && f, async::task<std::shared_ptr<CesiumVectorData::GeoJsonDocument>> && parent) Line 65
	at ..\build\vcpkg_installed\x64-windows-static\include\async++\task.h(65)
geo-raster-bug.exe!async::task<std::shared_ptr<CesiumVectorData::GeoJsonDocument>>::then<CesiumAsync::CesiumImpl::ImmediateScheduler<CesiumAsync::CesiumImpl::QueuedScheduler>,`CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider'::`2'::<lambda_1>>(CesiumAsync::CesiumImpl::ImmediateScheduler<CesiumAsync::CesiumImpl::QueuedScheduler> & sched, CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider::__l2::<lambda_1> && f) Line 263
	at ..\build\vcpkg_installed\x64-windows-static\include\async++\task.h(263)
geo-raster-bug.exe!CesiumAsync::Future<std::shared_ptr<CesiumVectorData::GeoJsonDocument>>::thenWithScheduler<`CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider'::`2'::<lambda_1>,CesiumAsync::CesiumImpl::ImmediateScheduler<CesiumAsync::CesiumImpl::QueuedScheduler>>(CesiumAsync::CesiumImpl::ImmediateScheduler<CesiumAsync::CesiumImpl::QueuedScheduler> & scheduler, const char * tracingName, CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider::__l2::<lambda_1> && f) Line 316
	at C:\dev\vcpkg\buildtrees\cesium-native\src\cb934da7c2-aa2bb1e783.clean\CesiumAsync\include\CesiumAsync\Future.h(316)
geo-raster-bug.exe!CesiumAsync::Future<std::shared_ptr<CesiumVectorData::GeoJsonDocument>>::thenInMainThread<`CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider'::`2'::<lambda_1>>(CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider::__l2::<lambda_1> && f) Line 97
	at C:\dev\vcpkg\buildtrees\cesium-native\src\cb934da7c2-aa2bb1e783.clean\CesiumAsync\include\CesiumAsync\Future.h(97)
geo-raster-bug.exe!CesiumRasterOverlays::GeoJsonDocumentRasterOverlay::createTileProvider(const CesiumRasterOverlays::CreateRasterOverlayTileProviderParameters & parameters) Line 832
	at C:\dev\vcpkg\buildtrees\cesium-native\src\cb934da7c2-aa2bb1e783.clean\CesiumRasterOverlays\src\GeoJsonDocumentRasterOverlay.cpp(832)
geo-raster-bug.exe!CesiumRasterOverlays::RasterOverlay::activate(const CesiumRasterOverlays::RasterOverlayExternals & externals, const CesiumGeospatial::Ellipsoid & ellipsoid) Line 94
	at C:\dev\vcpkg\buildtrees\cesium-native\src\cb934da7c2-aa2bb1e783.clean\CesiumRasterOverlays\src\RasterOverlay.cpp(94)
geo-raster-bug.exe!Cesium3DTilesSelection::RasterOverlayCollection::add(const CesiumUtility::IntrusivePointer<CesiumRasterOverlays::RasterOverlay const> & pOverlay) Line 57
	at C:\dev\vcpkg\buildtrees\cesium-native\src\cb934da7c2-aa2bb1e783.clean\Cesium3DTilesSelection\src\RasterOverlayCollection.cpp(57)

I can see you already filed cesium-native #1362, and our team has acknowledged it there. Thanks for doing that, and for the complete standalone reproduction, that kind of minimal repro really accelerates a fix.

In the meantime, a workaround might be to create a fresh GeoJsonDocumentRasterOverlay instance each time rather than re-adding the same one:

One thing that would help scope the fix: have you tried this with any other overlay types (like WebMapServiceRasterOverlay or IonRasterOverlay)? I believe this is specific to GeoJsonDocumentRasterOverlay based on where the stack terminates, but it would be good to confirm whether the issue is in this class specifically or in the base overlay activation path.

Cheers!