Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Source / tile architecture thoughts #8476

Closed
jfirebaugh opened this issue Mar 20, 2017 · 3 comments
Closed

Source / tile architecture thoughts #8476

jfirebaugh opened this issue Mar 20, 2017 · 3 comments

Comments

@jfirebaugh
Copy link
Contributor

This ticket is a meandering brain dump as I started looking into preparing our source- and tile-related architecture for asynchronous rendering.

At a high level, the goal is to draw a line between two different sets of runtime data based on thread affinity:

  1. The (unevaluated) style and (non-rendering-related parts) of sources live on the main thread, so that they can be queried and mutated via public APIs synchronously and without locking. I think of this as essentially a basic CRUD implementation of the domain objects from the style specification.
  2. Additional data required primarily for rendering lives on a separate thread. This includes tile data fetched from network or cache, and data calculated by evaluating style properties at the current camera position. Calculating which tiles to fetch and evaluating style properties both require access to the domain objects from (1), so the rendering thread must have either a copy of the main thread data, or a reference to an immutable shared object (I prefer the latter).

(From a design purity standpoint, it would be better to motivate this division based on good design principles like SRP rather than a preconceived notion of a particular threading model. But I think the result would be largely the same.)

On possible first step is to split Source::Impl along these lines, separating the implementation of the style specification notion of a Source from tile management and rendering-related tasks. A basic attempt at performing this split revealed many questions:

  • Where belongs the responsibility for requesting and revalidating source TileJSON? In gl-native, requesting this data happens only if the source is needed for rendering, and revalidating it may require invalidating previously parsed tiles and bucket data. This suggests that requesting and revalidating TileJSON should be the responsibility of the rendering side. However, this is in tension with issues such as Cannot introspect source or layer obtained from style #6584 and [core] Expose Source::getZoomRange #8472, which want to expose properties of the retrieved TileJSON as if they had been included inline in the original style JSON. In retrospect, I wish we had mandated inline TileJSON in the v8 style specification, as that would eliminate this tension.
  • How do we handle updating GeoJSON? Similarly to TileJSON, updating GeoJSON must clear the tile cache and prompt regeneration of bucket data for the currently visible tiles. The public API will clearly remain a setData method on a main-thread GeoJSONSource object. The corresponding rendering-side objects must be somehow notified of the mutation and efficiently provided with the new source data.
  • How do we handle feature querying? We've exposed a public synchronous API for querying both rendered and source features, but under the proposed division, both will need to round-trip from the main thread to the rendering thread.
  • The SourceObserver and TileObserver interfaces must be rationalized. This will require its own requirements investigation.
  • Does the rendering side require a hierarchy for its source-related classes that mirrors the class hierarchy for style specification classes? I.e. RenderSource, RenderTileSource, RenderVectorSource, RenderGeoJSONSource?
  • If the render thread must have both a Source object (which is a copy of or immutable reference to the main-side object), and a RenderSource, should the former own the latter, or vice versa?

When considering possible class designs for source and tile related classes, it's worth noting that gl-js has a design history we can draw on:

  • It currently has a division (in classes, not in threading) somewhat like what's proposed here. Specifically, it has both Source and SourceCache classes. Source is the style specification domain object; SourceCache has the tile loading logic.
  • An instance of SourceCache owns an instance of Source. This essentially inverts a relationship found in a prior revision of the gl-js source architecture, where there were Sources which owned TilePyramids.
  • However, we're not entirely happy with the current design.

Whenever I get deep into considering these types of questions, I find myself wanting to rebuild rendering from scratch in a purely functional style, where everything is a function accepting immutable inputs and calculating an immutable output and nothing is cached or stateful or side-effecting. (And when I say nothing, I mean nothing: the rewrite would start off by having tiles re-requested, re-parsed, re-bucketed on every frame.) Then see how to add back statefullness, caching, and reasonable performance in a controlled manner.

@anandthakker
Copy link
Contributor

How do we handle feature querying?

Noting that GL JS keeps a copy of vector tile (and feature index) data both on worker threads and the main thread for exactly this reason. (That's a different axis of separation than the main-vs-render split we're talking about here, but still a similar problem.)

@jfirebaugh jfirebaugh mentioned this issue Mar 21, 2017
9 tasks
@kkaefer
Copy link
Contributor

kkaefer commented Mar 23, 2017

Additional data required primarily for rendering lives on a separate thread. This includes tile data fetched from network or cache

I'd prefer to not think of this in a manner of threads, and instead of "who has access to what, and how does the information/ownership flow". The model I'd like to see us move to doesn't necessarily have a render thread, instead it has a render tree which is entirely self contained. This render tree could then be rendered on any thread (including the main thread in the case of iOS/macOS/GLFW), or a separate render thread (in the case of Android/Qt).

Control would still remain on the main thread, and we'd send new render objects via message passing. Whenever the UI system decides that it needs to render the frame, we empty the mailbox and integrate the new data into the render tree, potentially removing old data.

Currently, we are defining rendering by the tiles we have parsed. This leads to a number of problems:

  • When using multiple sources, label placement happens per source tile, even if multiple sources have data in the area
  • We need to support a large number of possible tile combination and compute "clip IDs" for all of them. Sometimes this leads to a "stencil mask overflow" (clip geometries before adding to buffer #229, stencil mask overflow. #962)
  • We sometimes see label flickering when tiles are evicted prematurely (Flickering labels when panning #7026)
  • We sometimes show new tiles without accompanying labels because they are still being parsed. This may generate the impression that no features exist at a particular location

Instead, I'd like to move to a model where we create metatiles/render tiles that represent the "ideal tiles", but are synthesized from various sources by creating buckets for all layers that are clipped to that render tile extent. The supporting meta data would still live on the main thread, but the actual GL objects would be created/maintained/owned on the GL "thread".

@tmpsantos
Copy link
Contributor

This render tree could then be rendered on any thread (including the main thread in the case of iOS/macOS/GLFW), or a separate render thread (in the case of Android/Qt).

To clarify, what Qt and Android expects here is pretty much a data structure like RenderTree and we would have something like GLPainter::render(RenderTree) and that would translate to GL commands. RenderTree should be immutable/thread-safe and GLPainter can live in any thread.

Having a clear line separating the RenderTree will be nice because we can also write render tree tests like WebKit (https://github.com/WebKit/webkit/tree/master/Tools/DumpRenderTree).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants