mapbox / mapbox-unity-sdk

Mapbox Unity SDK - https://www.mapbox.com/unity/
Other
721 stars 214 forks source link

Create a LoadBalancer for map creation #262

Closed david-rhodes closed 6 years ago

david-rhodes commented 6 years ago

Currently, all tile-related generation happens in a single frame, often for many tiles within the same frame (especially with caching). Ideally, we could distribute this work out over several frames so that we do not lock the main thread.

Suggestion: Use coroutines (Runnable.Run for use in non-monobehaviour classes) to distribute work over several frames. Perhaps we only process one tile per frame, or furthermore, one modifier per frame. For dense mesh construction, we may even have to process only n features per frame.

Reference: https://github.com/mapbox/mapbox-unity-sdk/issues/226 Inspiration: https://youtu.be/mQ2KTRn4BMI?t=9m54s

Thoughts @BergWerkGIS @brnkhy?

brnkhy commented 6 years ago

I totally agree, hacked VectorLayerVisualization a little this morning and was able to switch everything to coroutine there rather easily. That might be a starting point, for tests and stuff but I think we should start something like that higher in the hierarchy for a complete solution.

etown commented 6 years ago

Hi @brnkhy does your work exist on a fork or branch somewhere? I would be happy to help test and/or contribute.

brnkhy commented 6 years ago

@etown I'm afraid not. But we probably should start testing this right? I'll try to recreate it and push on a branch as soon as possible. Let's start playing with it already :)

etown commented 6 years ago

@brnkhy awesome! let's do it :)

brnkhy commented 6 years ago

@etown this is so simple that it probably doesn't worth a branch at this point; https://twitter.com/brnkhy/status/915232488351571968

Just a small change in VectorLayerVisualizer;

namespace Mapbox.Unity.MeshGeneration.Interfaces
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Mapbox.VectorTile;
    using UnityEngine;
    using Mapbox.Unity.MeshGeneration.Filters;
    using Mapbox.Unity.MeshGeneration.Data;
    using Mapbox.Unity.MeshGeneration.Modifiers;
    using Mapbox.Unity.Utilities;
    using System.Collections;

    [Serializable]
    public class TypeVisualizerTuple
    {
        public string Type;
        [SerializeField]
        public ModifierStackBase Stack;
    }

    /// <summary>
    /// VectorLayerVisualizer is a specialized layer visualizer working on polygon and line based vector data (i.e. building, road, landuse) using modifier stacks.
    /// Each feature is preprocessed and passed down to a modifier stack, which will create and return a game object for that given feature.
    /// Key is the name of the layer to be processed.
    /// Classification Key is the property name to be used for stack selection.
    /// It also supports filters; objects that goes over features and decides if it'll be visualized or not.
    /// Default Stack is the stack that'll be used for any feature that passes the filters but isn't matched to any special stack.
    /// 
    /// </summary>
    [CreateAssetMenu(menuName = "Mapbox/Layer Visualizer/Vector Layer Visualizer")]
    public class VectorLayerVisualizer : LayerVisualizerBase
    {
        [SerializeField]
        private string _classificationKey;
        [SerializeField]
        private string _key;
        public override string Key
        {
            get { return _key; }
            set { _key = value; }
        }

        [SerializeField]
        private List<FilterBase> Filters;

        [SerializeField]
        [NodeEditorElementAttribute("Default Stack")]
        public ModifierStackBase _defaultStack;
        [SerializeField]
        [NodeEditorElementAttribute("Custom Stacks")]
        public List<TypeVisualizerTuple> Stacks;

        //private GameObject _container;

        /// <summary>
        /// Creates an object for each layer, extract and filter in/out the features and runs Build method on them.
        /// </summary>
        /// <param name="layer"></param>
        /// <param name="tile"></param>
        public override void Create(VectorTileLayer layer, UnityTile tile)
        {
            Runnable.Run(NewMethod(layer, tile));
        }

        private IEnumerator NewMethod(VectorTileLayer layer, UnityTile tile)
        {
            var _container = new GameObject(Key + " Container");
            _container.transform.SetParent(tile.transform, false);

            //testing each feature with filters
            var fc = layer.FeatureCount();
            var filterOut = false;
            for (int i = 0; i < fc; i++)
            {
                filterOut = false;
                var feature = new VectorFeatureUnity(layer.GetFeature(i, 0), tile, layer.Extent);
                foreach (var filter in Filters)
                {
                    if (!string.IsNullOrEmpty(filter.Key) && !feature.Properties.ContainsKey(filter.Key))
                        continue;

                    if (!filter.Try(feature))
                    {
                        filterOut = true;
                        break;
                    }
                }

                if (!filterOut)
                    Build(feature, tile, _container);

                yield return null;
            }

            var mergedStack = _defaultStack as MergedModifierStack;
            if (mergedStack != null)
            {
                mergedStack.End(tile, _container);
                yield return null;
            }

            foreach (var item in Stacks)
            {
                mergedStack = item.Stack as MergedModifierStack;
                if (mergedStack != null)
                {
                    mergedStack.End(tile, _container);
                }
                yield return null;
            }
        }

        /// <summary>
        /// Preprocess features, finds the relevant modifier stack and passes the feature to that stack
        /// </summary>
        /// <param name="feature"></param>
        /// <param name="tile"></param>
        /// <param name="parent"></param>
        private bool IsFeatureValid(VectorFeatureUnity feature)
        {
            if (feature.Properties.ContainsKey("extrude") && !bool.Parse(feature.Properties["extrude"].ToString()))
                return false;

            if (feature.Points.Count < 1)
                return false;

            return true;
        }

        private void Build(VectorFeatureUnity feature, UnityTile tile, GameObject parent)
        {
            if (!IsFeatureValid(feature))
                return;

            //this will be improved in next version and will probably be replaced by filters
            var styleSelectorKey = FindSelectorKey(feature);

            var meshData = new MeshData();
            meshData.TileRect = tile.Rect;

            //and finally, running the modifier stack on the feature
            var mod = Stacks.FirstOrDefault(x => x.Type.Contains(styleSelectorKey));
            if (mod != null)
            {
                mod.Stack.Execute(tile, feature, meshData, parent, mod.Type);
            }
            else
            {
                if (_defaultStack != null)
                {
                    _defaultStack.Execute(tile, feature, meshData, parent, _key);
                }
            }
        }

        private string FindSelectorKey(VectorFeatureUnity feature)
        {
            if (string.IsNullOrEmpty(_classificationKey))
            {
                if (feature.Properties.ContainsKey("type"))
                {
                    return feature.Properties["type"].ToString().ToLowerInvariant();
                }
                else if (feature.Properties.ContainsKey("class"))
                {
                    return feature.Properties["class"].ToString().ToLowerInvariant();
                }
            }
            else if (feature.Properties.ContainsKey(_classificationKey))
            {
                if (feature.Properties.ContainsKey(_classificationKey))
                {
                    return feature.Properties[_classificationKey].ToString().ToLowerInvariant();
                }
            }

            return "";
        }
    }
}
brnkhy commented 6 years ago

I did a test with ThreadNinja (https://assetstore.unity.com/packages/tools/thread-ninja-multithread-coroutine-15717) and threaded mesh modifiers. I must say I have no idea how threadninja works but I believe it's a single background thread. extremely easy to use and was rather easy to plug into our system. without thread ninja 25 tiles takes around 3000ms with thread ninja 25 tiles takes around 2600ms it wasn't noticable of course and I spent vast majority of my time to change event system so I can get those numbers ><

brnkhy commented 6 years ago

pushed it all here anyway; https://github.com/mapbox/mapbox-unity-sdk/tree/CoroutineAndThreadNinjaTest you'll have to install ThreadNinja (free) from asset store to run it.

etown commented 6 years ago

@brnkhy this is really really great and almost completely solved my problem. do you think something similar could be done with raster tile initialization? (satellite layer)

brnkhy commented 6 years ago

@etown first of all, you might want to check this as well; I think this is the latest at the moment and official memory/perf branch; https://github.com/mapbox/mapbox-unity-sdk/tree/ModuleInitialize

imagery is heavy itself but I don't think there's a leak or freeze issues with that right? what kind of problems are you having with that?

etown commented 6 years ago

@brnkhy I am still experiencing some ui blocking on initialization when there is imagery, but much less so when it's just vector. I will try the branch, and create a minimal map with 9 titles of satellite imagery. should be able to load without blocking, correct? thanks !

brnkhy commented 6 years ago

@etown I haven't noticed that before but I think I haven't been looking into imagery too much so I might have missed that. Please let me know if you still have it even with that second branch, I'll also check it myself.

david-rhodes commented 6 years ago

@etown With 9 tiles, I don't imagine the strain would be too much for current mobile devices. What sort of map are you building? Streaming (slippy)?

A few things you can do:

  1. Write a tile provider that requests n tiles per frame. This ensures only so many textures are processed each frame.
  2. You could divide the work over multiple frames inside VectorLayerVisualizer or MapImageFactory using Runnable.Run.

Edit: we'll also look into adding these features, but these notes are for your use in the meantime.

MiroMuse commented 6 years ago

This ticket looks done, so closing out! Feel free to reopen at any time.