diff --git a/CHANGES.md b/CHANGES.md index b27f7ae62..9748cac2d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,11 +2,18 @@ ### ? - ? +##### Breaking Changes :mega: + +- `Tileset::updateViewGroupOffline`, `Tileset::updateViewGroup`, and `Tileset::loadTiles` no longer call `AsyncSystem::dispatchMainThreadTasks`. Client applications must call that function themselves from their main thread. + +##### Additions :tada: + +- Added a constructor overload for `Cesium3DTilesSelection::ITwinCesiumCuratedContentLoaderFactory` to override the iTwin Cesium Curated Content base URL. This makes it possible to connect to alternate servers (e.g., staging, QA, mock servers). + ##### Fixes :wrench: - `Cesium3DTilesetSelection::Tileset::getRootTileAvailableEvent` will now resolve even when a `Tileset` is constructed with invalid source parameters, instead of hanging indefinitely. - Fixed compilation error with MSVC when using custom `CesiumITwinClient::PagedList` types. -- Ability to override the iTwin Cesium Curated Content base URL used by `iTwinCesiumCuratedContentLoaderFactory` to make it possible to connect to alternate servers (e.g., staging, QA, mock servers). ### v0.58.0 - 2026-03-02 diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index 9b32b760c..58ad0eb25 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -434,10 +434,6 @@ class CESIUM3DTILESSELECTION_API Tileset final { * In order to minimize tile load latency, this method should be called * frequently, such as once per render frame. It will return quickly when * there is no work to do. - * - * This method also calls - * {@link CesiumAsync::AsyncSystem::dispatchMainThreadTasks} on the tileset's - * {@link getAsyncSystem}. */ void loadTiles(); diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 4cb1275c9..3bd861faf 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -396,9 +396,6 @@ const ViewUpdateResult& Tileset::updateViewGroup( _options.enableFrustumCulling && !_options.enableLodTransitionPeriod; _options.enableFogCulling = _options.enableFogCulling && !_options.enableLodTransitionPeriod; - - this->_asyncSystem.dispatchMainThreadTasks(); - ViewUpdateResult& result = viewGroup.getViewUpdateResult(); Tile* pRootTile = this->_pTilesetContentManager->getRootTile(); @@ -444,9 +441,6 @@ const ViewUpdateResult& Tileset::updateViewGroup( void Tileset::loadTiles() { CESIUM_TRACE("Tileset::loadTiles"); - - this->_asyncSystem.dispatchMainThreadTasks(); - Tile* pRootTile = this->_pTilesetContentManager->getRootTile(); if (!pRootTile) { // If the root tile is marked as ready, but doesn't actually exist, then diff --git a/Cesium3DTilesSelection/test/TestTileLoadRequester.cpp b/Cesium3DTilesSelection/test/TestTileLoadRequester.cpp index b7f40445b..15873261e 100644 --- a/Cesium3DTilesSelection/test/TestTileLoadRequester.cpp +++ b/Cesium3DTilesSelection/test/TestTileLoadRequester.cpp @@ -145,6 +145,7 @@ TEST_CASE("TileLoadRequester") { // Request this tile for main thread loading and verify it happens. requester.setMainThreadQueue({pToLoad}); + externals.asyncSystem.dispatchMainThreadTasks(); pTileset->loadTiles(); CHECK(pToLoad->getState() == TileLoadState::Done); } diff --git a/Cesium3DTilesSelection/test/TestTilesetHeightQueries.cpp b/Cesium3DTilesSelection/test/TestTilesetHeightQueries.cpp index c0833449e..0e935f2ee 100644 --- a/Cesium3DTilesSelection/test/TestTilesetHeightQueries.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetHeightQueries.cpp @@ -65,6 +65,7 @@ TEST_CASE("Tileset height queries") { Cartographic::fromDegrees(-75.612025, 40.041684, 0.0)}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -104,6 +105,7 @@ TEST_CASE("Tileset height queries") { Cartographic::fromDegrees(-75.612025, 40.041684, 0.0)}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -138,6 +140,7 @@ TEST_CASE("Tileset height queries") { Cartographic::fromDegrees(-75.612025, 40.041684, 0.0)}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -177,6 +180,7 @@ TEST_CASE("Tileset height queries") { Cartographic::fromDegrees(-75.612025, 40.041684, 0.0)}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -212,6 +216,7 @@ TEST_CASE("Tileset height queries") { {Cartographic::fromDegrees(-75.612559, 40.042183, 0.0)}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -231,6 +236,7 @@ TEST_CASE("Tileset height queries") { {Cartographic::fromDegrees(-75.612559, 40.042183, 0.0)}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -250,6 +256,7 @@ TEST_CASE("Tileset height queries") { {Cartographic::fromDegrees(-75.612559, 40.042183, 1.0)}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); pTileset->loadTiles(); } @@ -292,6 +299,7 @@ TEST_CASE("Tileset height queries") { {Cartographic::fromDegrees(10.0, 45.0, 0.0)}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -330,6 +338,7 @@ TEST_CASE("Tileset height queries") { tileset.sampleHeightMostDetailed({samplePosition}); while (!future.isReady()) { + externals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } diff --git a/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp b/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp index e4c29278a..fea0f96e7 100644 --- a/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp @@ -93,8 +93,9 @@ static void initializeTileset(Tileset& tileset) { horizontalFieldOfView, verticalFieldOfView, Ellipsoid::WGS84); - + tileset.getExternals().asyncSystem.dispatchMainThreadTasks(); tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tileset.getExternals().asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -219,9 +220,11 @@ TEST_CASE("Test replace refinement for render") { // Check 1st and 2nd frame. Root should meet sse and render. No transitions // are expected here for (int frame = 0; frame < 2; ++frame) { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup( tileset.getDefaultViewGroup(), {zoomOutViewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // Check tile state. Ensure root meet sse @@ -281,8 +284,10 @@ TEST_CASE("Test replace refinement for render") { // 1st frame. Root doesn't meet sse, so it goes to children. But because // children haven't started loading, root should be rendered. { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // Check tile state. Ensure root doesn't meet sse, but children does. @@ -309,8 +314,10 @@ TEST_CASE("Test replace refinement for render") { // 2nd frame. Because children receive failed response, so they will be // rendered as empty tiles. { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // Check tile state. Ensure root doesn't meet sse, but children does @@ -354,9 +361,11 @@ TEST_CASE("Test replace refinement for render") { // 1st frame. Root doesn't meet sse, but none of the children finish // loading. So we will render root { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup( tileset.getDefaultViewGroup(), {zoomInViewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // check tiles status. All the children should have loading status @@ -388,9 +397,11 @@ TEST_CASE("Test replace refinement for render") { // 2nd frame. All the children finish loading, so they are ready to be // rendered (except ll.b3dm tile since it doesn't meet sse) { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup( tileset.getDefaultViewGroup(), {zoomInViewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // check tiles status. All the children should have loading status @@ -448,6 +459,7 @@ TEST_CASE("Test replace refinement for render") { ViewUpdateResult result = tileset.updateViewGroup( tileset.getDefaultViewGroup(), {zoomOutViewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // check tiles status. All the children should have loading status @@ -495,8 +507,10 @@ TEST_CASE("Test replace refinement for render") { // none of the children are loaded, root will be rendered instead and // children transition from unloaded to loading in the mean time { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // Check tile state. Ensure root doesn't meet sse, but children does @@ -522,8 +536,10 @@ TEST_CASE("Test replace refinement for render") { // 2nd frame. Children are finished loading and ready to be rendered. Root // shouldn't be rendered in this frame { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // check tile states @@ -620,8 +636,10 @@ TEST_CASE("Test additive refinement") { // 1st frame. Root, its child, and its four grandchildren will all be // rendered because they meet SSE, even though they're not loaded yet. { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); const std::vector& ttr = @@ -656,8 +674,10 @@ TEST_CASE("Test additive refinement") { // 2nd frame { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); const std::vector& ttr = @@ -715,6 +735,7 @@ TEST_CASE("Test additive refinement") { { ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); REQUIRE(result.tilesToRenderThisFrame.size() == 8); @@ -786,8 +807,10 @@ TEST_CASE("Render any tiles even when one of children can't be rendered for " // 1st frame. Root doesn't meet sse, so load children. But they are // non-renderable, so render root only { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); for (const Tile& child : root->getChildren()) { @@ -805,8 +828,10 @@ TEST_CASE("Render any tiles even when one of children can't be rendered for " // 2nd frame. Root doesn't meet sse, so load children. Even one of the // children is failed, render all of them even there is a hole { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); REQUIRE(root->isRenderable()); @@ -904,9 +929,11 @@ TEST_CASE("Test multiple frustums") { // frame 1 { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup( tileset.getDefaultViewGroup(), {viewState, zoomOutViewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // Check tile state. Ensure root meets sse for only the zoomed out @@ -926,9 +953,11 @@ TEST_CASE("Test multiple frustums") { // frame 2 { + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); ViewUpdateResult result = tileset.updateViewGroup( tileset.getDefaultViewGroup(), {viewState, zoomOutViewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // Check tile state. Ensure root meets sse for only the zoomed out @@ -996,8 +1025,8 @@ TEST_CASE("Test multiple frustums") { ViewUpdateResult result = tileset.updateViewGroup( tileset.getDefaultViewGroup(), {zoomInViewState1, zoomInViewState2}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); - // check result // The grand child and the second child are the only ones rendered. // The third and fourth children of the root are culled. @@ -1288,6 +1317,7 @@ TEST_CASE("Makes metadata available on external tilesets") { tileset.updateViewGroup( tileset.getDefaultViewGroup(), {zoomToTileViewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); pExternalContent = pExternal->getContent().getExternalContent(); } @@ -1745,6 +1775,7 @@ TEST_CASE("Additive-refined tiles are added to the tilesFadingOut array") { tileset.computeLoadProgress() < 100.0f) { updateResult = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {viewState}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); } @@ -1765,6 +1796,7 @@ TEST_CASE("Additive-refined tiles are added to the tilesFadingOut array") { Ellipsoid::WGS84); updateResult = tileset.updateViewGroup(tileset.getDefaultViewGroup(), {zoomedOut}); + tilesetExternals.asyncSystem.dispatchMainThreadTasks(); tileset.loadTiles(); // Only the root tile (plus the tileset.json) is visible now, and the other diff --git a/doc/topics/async-system.md b/doc/topics/async-system.md index 5e6e78382..33d92b65b 100644 --- a/doc/topics/async-system.md +++ b/doc/topics/async-system.md @@ -56,7 +56,7 @@ In practice, though, we almost never call `wait` or `waitInMainThread`. After al A continuation function is a callback - usually a lambda - that is invoked when the `Future` resolves. The name comes from the idea that the execution "continues" with that function after the async operation. Continuations are registered with the `then...` methods on the `Future`. Different `then` functions are used to control _in what thread_ the callback is invoked: -* [thenInMainThread](@ref CesiumAsync::Future::thenInMainThread): The continuation function is invoked in the "main" thread. Many applications have a clear idea of which thread they consider the main one, but in Cesium Native, "main thread" very specifically means the thread that called `waitInMainThread` or [dispatchMainThreadTasks](@ref CesiumAsync::AsyncSystem::dispatchMainThreadTasks) on the `AsyncSystem`. This need not be the same as the thread your application considers to be the main one. +* [thenInMainThread](@ref CesiumAsync::Future::thenInMainThread): The continuation function is invoked in the "main" thread. Many applications have a clear idea of which thread they consider the main one, but in Cesium Native, "main thread" very specifically means the thread that called `waitInMainThread` or [dispatchMainThreadTasks](@ref CesiumAsync::AsyncSystem::dispatchMainThreadTasks) on the `AsyncSystem`. This need not be the same as the thread your application considers to be the main one; however, your application must call `dispatchMainThreadTasks` periodically from a unique thread in order for main thread continuation functions to run at all. * [thenInWorkerThread](@ref CesiumAsync::Future::thenInWorkerThread): The continuation function is invoked in a background worker thread by calling [startTask](@ref CesiumAsync::ITaskProcessor::startTask) on the `ITaskProcessor` instance with which the `AsyncSystem` was constructed. * [thenInThreadPool](@ref CesiumAsync::Future::thenInThreadPool): The continuation function is invoked in a specified [ThreadPool](@ref CesiumAsync::ThreadPool). This is most commonly used with a `ThreadPool` containing just a single thread, in order to delegate certain tasks (such as database access) to a dedicated thread. In other cases, `thenInWorkerThread` is probably a better choice. * [thenImmediately](@ref CesiumAsync::Future::thenImmediately): The continuation function is invoked immediately in whatever thread happened to resolve the `Future`. This is only appropriate for work that completes very quickly and is safe to run in any thread.