第18章 - mbgl-core 源码深入
本章标题沿用“mbgl-core”的习惯叫法,指 core C++ 库本身;main 分支的源码目录与命名空间已 rename 为
src/mln/namespace mln,正文以 mln 为主,并给出 mbgl → mln 的对照。
18.1 先看命名:mbgl 还是 mln
打开旧文章或旧代码,会看到 include/mbgl、namespace mbgl、MBGL_* 宏;当前 main 分支已经统一改名:
| 旧 | 当前 |
|---|---|
include/mbgl/... |
include/mln/... |
src/mbgl/... |
src/mln/... |
namespace mbgl |
namespace mln |
MBGL_WITH_*(CMake 变量) |
MLN_WITH_* |
MBGL_BUILDING_LIB 等编译宏 |
MLN_* 前缀(历史宏仍可见于个别条件编译) |
但二进制目标名没有改:mbgl-core、mbgl-test-runner、mbgl-render-test-runner、mbgl-glfw、mbgl-render 仍然是 mbgl- 前缀。所以搜索代码时两种前缀都要试:头文件用 mln/,构建产物用 mbgl-。
本章标题沿用章节文件名里的 “mbgl-core”,指的是同一份 core 库。下文路径一律以当前 src/mln 为准。
18.2 core 的分层
src/mln 下 20 多个目录可以按职责切成五层:
平台绑定层(platform/ 下:android / ios / macos / node / qt / linux ...)
│ 只依赖 public include/mln 接口
▼
地图门面 map/ Map, Map::Impl, Transform, TransformState, MapProjection
│
├──────────────┬─────────────────┬──────────────────┐
▼ ▼ ▼ ▼
样式与数据 渲染编排 瓦片生产 基础设施
style/ renderer/ tile/ actor/ util/ math/
parser, diff orchestrator TilePyramid Scheduler Mailbox
RenderTree TileCache RunLoop geometry
RenderLayer GeometryTileWorker gfx/ gl/ mtl/
│
▼
后端抽象 gfx/ RendererBackend / Context / ShaderRegistry / Drawable
gl/、mtl/、vulkan/、webgpu/ 四个具体实现
一个关键设计是 ::Impl 模式:Map / Style / Layer / Source 这些公开类只是薄壳,真正的状态在对应的 Impl 里,且是 Immutable<T>(不可变共享引用)。改动样式时不是原地修改,而是”复制一份 Impl、改副本、冻结、换指针”。这样渲染线程拿到的永远是一份某个时刻的快照,不需要锁。
18.3 一帧的生命周期
入口在 src/mln/map/map_impl.cpp 的 Map::Impl::onUpdate()。它做三件事:
- 按需更新相机过渡:
transform.updateTransitions(timePoint)。 - 组装
UpdateParameters——这是 core 里最重要的结构体,相当于”这一帧的输入快照”。字段覆盖了渲染需要的全部状态:
UpdateParameters params = {.styleLoaded = style->impl->isLoaded(),
.mode = mode,
.pixelRatio = pixelRatio,
.timePoint = timePoint,
.transformState = transform.getState(),
.glyphURL = style->impl->getGlyphURL(),
.spriteLoaded = style->impl->areSpritesLoaded(),
.light = style->impl->getLight()->impl,
.images = style->impl->getImageImpls(),
.sources = style->impl->getSourceImpls(),
.layers = style->impl->getLayerImpls(),
.globalState = style->impl->getGlobalStateShared(),
.annotationManager = annotationManager.makeWeakPtr(),
.fileSource = fileSource,
.prefetchZoomDelta = prefetchZoomDelta,
.tileLodMode = tileLodMode};
- 交给
rendererFrontend.update(...)。
注意 if (mode != MapMode::Continuous && !stillImageRequest) return;——静态模式下不会自己渲染,必须显式请求一帧。这解释了为什么离屏渲染使用 MapMode::Static 时要先设置相机再 renderStill()。
Renderer::render() 本身只有三行(src/mln/renderer/renderer.cpp):
if (auto renderTree = impl->orchestrator.createRenderTree(updateParameters, impl->dynamicTextureAtlas)) {
renderTree->prepare();
impl->render(*renderTree, updateParameters);
}
真正的复杂度都在 createRenderTree() 里。
18.4 RenderOrchestrator:diff 驱动的渲染树
src/mln/renderer/render_orchestrator.cpp 的 createRenderTree() 是整个 core 最核心的函数。它把”上一帧的渲染树”与”这一帧的 UpdateParameters 快照”做差,只重建变化部分。核心 diff 全部来自 style_diff.hpp:
| 函数 | 比较对象 | 用途 |
|---|---|---|
diffLayers |
旧/新 Layer::Impl 列表 |
增删图层、触发重布局 |
diffSources |
旧/新 Source::Impl 列表 |
增删数据源 |
diffImages |
旧/新 Image::Impl 列表 |
图片增删改,触发重布局 |
diffGlobalStateKeys |
新旧 GlobalStateMap |
找出变化的 global-state 键 |
diff 能高效工作的前提就是 Impl 不可变:两个 Immutable<T> 指向同一对象说明没变,指针不同说明变了;集合本身是不可变集合时,指针相同就整批跳过。
由 diff 结果驱动的动作有三类:
layersAddedOrRemoved或样式属性变化 → 标记needsRelayout,交给 TilePyramid 重新生成 bucket。- 图片变化 →
hasImageDiff置位,同样触发重布局(图片参与 pattern 的图集打包)。 - 相机变化 → 只更新
PaintParameters,不重建几何。
渲染树的节点类型在 src/mln/renderer/render_tree.hpp:RenderItem 是抽象基类,LayerRenderItem 包装一个 RenderLayer 加它所属的 RenderSource 和绘制序号,RenderTree 把若干 RenderItem 按序排好,prepare() 之后由 Renderer::impl->render() 逐项 upload() + render()。
18.5 TilePyramid:瓦片金字塔的调度
src/mln/renderer/tile_pyramid.cpp(约 460 行)负责一个数据源的瓦片集合。核心是 update(),签名长达九个参数,但逻辑可以拆成四段:
void TilePyramid::update(const std::vector<Immutable<style::LayerProperties>>& layers,
const bool needsRendering,
const bool needsRelayout,
const TileParameters& parameters,
const style::Source::Impl& sourceImpl,
const uint16_t tileSize,
const Range<uint8_t> zoomRange,
std::optional<LatLngBounds> bounds,
std::function<std::unique_ptr<Tile>(const OverscaledTileID&, TileObserver*)> createTile);
第一段:清理。 needsRelayout 为真时直接 cache.clear();不需要渲染时把所有在屏瓦片移入缓存(TileNecessity::Optional,抑制过期时的网络请求)或延迟释放,然后清空 tiles、renderedTiles。
第二段:算要哪些瓦片。 关键三步:
int32_t overscaledZoom = util::coveringZoomLevel(zoom, type, tileSize);
int32_t idealZoom = std::min<int32_t>(zoomRange.max, overscaledZoom);
panTiles = util::tileCover(tileCoverParameters, panZoom, zoomRange);
idealTiles = util::tileCover(tileCoverParameters, idealZoom, zoomRange, tileZoom);
coveringZoomLevel 根据数据源类型(矢量/栅格/DEM/GeoJSON)和 tileSize 决定用哪一级瓦片;tileCover 根据当前 TransformState(含 pitch、bearing、fov)算出视锥覆盖的瓦片集合。开了 prefetchZoomDelta 时还会额外算 panTiles——低一级的瓦片,用来在高层瓦片到达前先垫一层。
第三段:保留与创建。 两个 lambda 分工明确:
retainTileFn:把要用的瓦片记进retain集合,设置必要性与更新参数;重布局时顺便tile.setLayers(layers, parameters.globalState)。createTileFn:先从缓存cache.pop(tileID)复用,没有再调外部注入的createTile工厂创建。这是TilePyramid与具体瓦片类型解耦的地方——矢量、栅格、DEM、自定义几何都走同一个金字塔,只是工厂不同。
第四段:驱动。 核心算法在 src/mln/algorithm/update_renderables.hpp,updateRenderables() 接收四个回调(get/create/retain/render),在”理想瓦片未就绪时用父/子瓦片顶替”的逻辑上做统一处理。这也是为什么地图加载过程中能看到低分辨率瓦片先出现再被替换。
缓存本身是 src/mln/tile/tile_cache.cpp:按 OverscaledTileID 索引,分 tiles(在屏)与 cache(离屏)两层,通过 TileNecessity(Required/Optional)区分优先级。
18.6 Transform 与 TransformState:相机
src/mln/map/transform.cpp 处理”相机动作”,transform_state.hpp 保存”相机状态”。
Transform 的公开方法就是 camera API:jumpTo(立即)、easeTo(线性过渡)、flyTo(先拉远再飞入)、moveBy、rotateBy、setLatLngBounds、setMinZoom/setMaxZoom、setPitch 等。它们最终都改 TransformState:
class Transform : private util::noncopyable {
TransformState state;
TimePoint transitionStart;
Duration transitionDuration;
std::function<bool(const TimePoint)> transitionFrameFn;
// ...
};
过渡不是每帧插值一个”目标值”,而是存一个 transitionFrameFn,每帧用当前时间求值。easeTo 与 flyTo 的区别只在插值函数上:flyTo 会构造一条”缩放先降后升”的曲线,所以视觉上有拉远再拉近的过程。
TransformState 里最容易被忽略的是 constrain 与 viewport 模式:ConstrainMode(是否允许视图越界)、ViewportMode(是否为宽视口调整中心)、NorthOrientation(允许把地图旋转 90° 用于车载/导航场景)。这些不是 UI 细节,而是直接影响 tileCover 结果的输入。
投影与坐标换算在 src/mln/map/map_projection.cpp;具体投影实现藏在 util/geo.hpp 与 ProjectionMode 里(Web Mercator / globe / 垂直透视等)。
18.7 Style:解析主链路
Style 的加载分两条路径,最终汇合到同一个解析器(src/mln/style/style_impl.cpp → src/mln/style/parser.cpp):
Style::loadURL(url) Style::loadJSON(json)
│ FileSource 异步请求 │
▼ ▼
Impl::loadURL 回调拿到 JSON ──────▶ Parser::parse(json)
├─ parseTransition
├─ parseLight
├─ parseState (global-state 默认值)
├─ parseSprites
├─ parseSources
└─ parseLayers
└─ parseLayer → 逐层构造 Layer + Layer::Impl
Parser 的成员就是解析结果:sources、layers、sprites、glyphURL、fontFaces、transition、light、globalStateDefaults,以及根级的 name/latLng/zoom/bearing/pitch/roll。返回类型是 StyleParseResult = std::exception_ptr——解析不抛异常,用 exception_ptr 回传错误。
一个细节:parseLayers 维护了 layersMap 和 stack(std::forward_list<std::string>),后者用于检测图层的 ref 引用环(Layer 可以引用另一个 Layer 的属性,成环会导致无限递归)。
解析完成后,Style 还要等异步资源:sprite、glyph、source 的元数据。Style::isLoaded 只表示 JSON 解析完成,不等待资源(这是 main 分支新增的语义,见 CHANGELOG);真正的”可渲染”要等 areSpritesLoaded()、styleLoaded 等条件同时满足。
18.8 后端抽象:gfx 与四个实现
src/mln/gfx/ 是与图形 API 无关的中间层:Context、ShaderRegistry、Drawable、RenderPass、纹理与 buffer 抽象。具体后端在 gl/、mtl/、vulkan/、webgpu/ 四个目录,通过 RendererBackend 的纯虚函数接入:
class RendererBackend {
public:
virtual Renderable& getDefaultRenderable() = 0;
virtual void initShaders(gfx::ShaderRegistry&, const ProgramParameters&) = 0;
protected:
virtual std::unique_ptr<Context> createContext() = 0;
virtual void activate() = 0;
virtual void deactivate() = 0;
};
getContext() 用 std::call_once 延迟创建上下文,并 assert(BackendScope::exists()) 强制”只能在 BackendScope 里访问上下文”。activate/deactivate 由 BackendScope 成对调用,覆盖两种场景:渲染一帧时、以及销毁 Renderer 释放 GPU 资源时。
这层设计的意义:core 的渲染逻辑(bucket → drawable → draw call)不感知后端,换 Metal/Vulkan/WebGPU 只需实现 RendererBackend + 一套 ShaderRegistry 与 shader 源码。
18.9 线程模型
core 不碰平台线程,只依赖 Scheduler 抽象(src/mln/actor/scheduler.hpp):
| 调度器 | 语义 | 用途 |
|---|---|---|
Scheduler::GetCurrent() |
当前线程的调度器,通常是 RunLoop |
主线程/渲染线程 |
Scheduler::GetBackground() |
共享线程池,任务可并行 | 瓦片解析、网络回调 |
Scheduler::GetSequenced() |
单线程、任务顺序执行 | 需要串行化的对象 |
RunLoop(include/mln/util/run_loop.hpp)是默认实现:内部两个队列(highPriorityQueue/defaultQueue),run() 阻塞处理,runOnce() 单步。它提供 setPlatformCallback,让”没有完整 run loop 集成”的平台在任务入队时收到通知,自行决定何时调 runOnce()——Android 的 GLThread、iOS 的 GCD 就是这么接进来的。
Actor 层(src/mln/actor/)用 Mailbox + ActorRef 把”对象必须在自己线程上被调用”这件事变成类型约束:每个 actor 的消息排队串行执行。GeometryTileWorker、RasterTileWorker、RenderSource 都是 actor。
18.10 读源码的路线
面对 2000+ 文件,按”从入口往下”的顺序读效率最高:
1. include/mln/map/map.hpp 公开 API 面
2. src/mln/map/map_impl.cpp onUpdate() → UpdateParameters
3. src/mln/renderer/renderer.cpp render() 三行
4. src/mln/renderer/render_orchestrator.cpp createRenderTree():diff → 更新
5. src/mln/renderer/tile_pyramid.cpp update():tileCover → retain/create
6. src/mln/algorithm/update_renderables.hpp 父/子瓦片顶替算法
7. src/mln/style/parser.cpp Style JSON → Impl 树
8. src/mln/map/transform.cpp camera → TransformState
9. src/mln/gfx/renderer_backend.hpp 后端接入点
调试时常用的三个切入口:UpdateParameters 的字段(判断”这一帧为什么重绘”)、diffLayers 的结果(判断”哪个图层触发了重布局”)、TilePyramid::getTiles()(判断”该有的瓦片在不在”)。
18.11 排错
编译报 fatal error: 'mbgl/...' file not found。 代码还是旧命名。当前头文件统一是 #include <mln/...>,CMake 选项是 MLN_WITH_*;但目标名仍是 mbgl-core,两者不要混。
改了样式属性但画面不更新。 检查是否绕过了 setXxx 的 Impl 复制流程,直接改了被 Immutable 包裹的对象(编译期一般会被 const 拦住);或者 needsRelayout 没被置位——diff 认为属性没变。
瓦片一直不显示,日志里没有错误。 看 TilePyramid::isLoaded() 与 tileRange:如果数据源设了 bounds,超出范围的瓦片在 createTileFn 里直接返回 nullptr,不会报错。
assert(BackendScope::exists()) 触发。 在 BackendScope 之外调了 getContext()。所有 GPU 资源操作都要在 BackendScope 生命周期内。
多线程下偶发崩溃。 检查对象是否跨线程直接访问,而不是通过 Scheduler/actor 投递。core 的约定是”对象只在所属线程被调用”,违反它的代码在单测里可能通过,在真机上会随任务调度概率性失败。
Style::isLoaded() 返回 true 但首帧空白。 它只代表 JSON 解析完成。首帧还需要 sprite、glyph、source 元数据就绪,用 MapObserver::onDidFinishLoadingStyle 或 onDidBecomeIdle 判断更可靠。
18.12 本章小结
| 要点 | 结论 |
|---|---|
| 命名 | 头文件与命名空间是 mln,构建目标仍是 mbgl-*,CMake 选项是 MLN_* |
| 分层 | 平台绑定 → map 门面 → style/renderer/tile → gfx 后端抽象 |
| Impl 模式 | 公开类是壳,状态在不可变 Impl 里,靠换指针实现线程安全的样式变更 |
| 一帧输入 | UpdateParameters,由 Map::Impl::onUpdate() 组装 |
| 渲染树 | RenderOrchestrator::createRenderTree(),用 diffLayers/diffSources/diffImages 做增量更新 |
| 瓦片 | TilePyramid::update():tileCover 算集合 → retain/create → updateRenderables 顶替 |
| 相机 | Transform 管动作与过渡,TransformState 是状态,含 constrain/viewport/projection 模式 |
| 样式 | Parser::parse() 产出 source/layer/sprite/glyph/light/globalState,引用环用 stack 检测 |
| 后端 | RendererBackend 四个纯虚函数,BackendScope 管上下文生命周期 |
| 线程 | Scheduler 三档 + RunLoop + actor/mailbox,core 不直接创建线程 |