20 光照抑制现象
1 机制前置
本章只讨论光照抑制现象本身。相关机制分散在三个前置章节中:
- 生成状态与 ChunkStatus:
INITIALIZE_LIGHT/LIGHT阶段、taskMargin=1、光照如何阻塞FULL。 - 异步任务与三线程模型:light actor、
ServerLightingProvider.pendingTasks、LevelPrioritizedQueue、1000 批处理上限。 - 加载票机制:START、PLAYER、LIGHT 票以及 level 传播。
2 START 与 PLAYER 票的执行时序差异:光照抑制器效率分析
本节把 "光照抑制器效率" 定义为 LQ 净增长速率。这里的 LQ 指 ServerLightingProvider.pendingTasks 以及它前面的 ChunkTaskPrioritySystem 优先级队列所共同形成的光照任务积压;效率越高,不是指光照处理越快,而是指积压增长越快,区块加载越容易被 ChunkStatus.LIGHT 卡住。
2.1 LQ 净增长速率:抑制器效率的真实定义
入队速度由机器 tick 频率、每次机器更新制造的光照更新数、这些更新被包装成光照任务的时序共同决定。出队速度由 ServerLightingProvider.runTasks() 限制:line 196 先取 Math.min(this.pendingTasks.size(), 1000),line 200-217 只围绕这 i 个任务执行 PRE_UPDATE、super.doLightUpdates() 和 POST_UPDATE。
所以在 PLAYER 正常 tick 推进时,光照任务的有效出队速度固定为 ≤ 1000 任务 / gt。当机器制造光照任务的入队速度高于这个上限,LQ 净增长速率 > 0,队列积压增长,抑制器效率高;当入队被优先级、加载时序或阻塞式清空分散到低于出队能力,LQ 净增长速率 ≤ 0,积压无法持续增长,抑制器效率低。
2.2 光照任务的优先级队列机制
光照任务不是单纯按区块 tick 的先后直接执行。ServerLightingProvider.enqueue() 在 line 132-137 把任务交给 ChunkTaskPrioritySystem.createMessage(),并传入区块坐标和 completedLevelSupplier:
createMessage() 在 ChunkTaskPrioritySystem.java line 49-53 把 Runnable 包装成 Task<Runnable>,保留 pos 和 lastLevelUpdatedToProvider。随后 enqueueChunk() 在 line 124-130 读取 lastLevelUpdatedToProvider.getAsInt(),把任务以这个 level 插入 LevelPrioritizedQueue。
LevelPrioritizedQueue 的核心数据结构是按 level 分桶、每个 level 内再按区块坐标保存插入顺序的链式 map:
出队时,poll() 直接从 firstNonEmptyLevel 开始;level 数字越小,加载等级越强,优先级越高。进入同一个 level 后,line 93 的 firstLongKey() 和 line 94 的 removeFirst() 取的是该 Long2ObjectLinkedOpenHashMap 中最早插入的区块。
结论是确定的:光照任务先按 level 从小到大处理;同 level 内按区块首次进入该 level 桶的插入顺序处理;同一区块下的多个任务保存在同一个 List<Optional<T>> 中,随该区块一起被取出。
2.3 区块 tick 与光照任务的解耦
ServerChunkManager.tickChunks() 会把本 tick 要执行区块 tick 的 WorldChunk 收集到 list,然后在 line 385 调用 Collections.shuffle(list):
这只随机化 "哪些区块先 tick 并产生光照任务"。任务产生后进入 ChunkTaskPrioritySystem,处理顺序改由 LevelPrioritizedQueue.poll() 的 level 和插入顺序决定。因此,区块 tick 随机化会影响入队的瞬时分布,却不会覆盖光照任务队列的出队规则。
2.4 机器分布对 LQ 净增速的影响
光照抑制器的核心目标是让光照任务队列持续积压(LQ > 0),使区块卡在 LIGHT 阶段无法推进到 FULL。分布方式决定了积压的持久性。
2.4.1 梯度分布(不同 level)
梯度分布指机器落在 level = 22、23、24、...、31 等不同加载等级的区块中。由于 LevelPrioritizedQueue.poll() 固定从 firstNonEmptyLevel 取任务,低 level 的任务先被处理,高 level 的任务在队列后方等待。
这种分布会形成 "优先级阻塞":
- level = 22 的机器任务优先进入处理通道
- level = 23 的任务必须等 level = 22 清空后才能出队
- level = 31 的任务排在最后
只要持续有新的低 level 任务入队,高 level 任务就会被 "推" 到队列后方,形成持久积压。即使出队速度 ≥ 入队速度(LQ 净增 ≤ 0),高 level 区块的光照任务仍然在队列中堆积,无法及时完成。
因此,梯度分布下抑制器效率高—— 不是因为队列总长度增长,而是因为目标区块的光照任务被低优先级任务 "卡" 在队列后方。
2.4.2 平坦分布(同一 level)
平坦分布指机器集中在同一加载等级,例如玩家视距内大量 level = 31 的强加载区块。所有机器任务拥有相同优先级,LevelPrioritizedQueue 只在同 level 内按区块插入顺序(FIFO)推进。
同 level 内没有优先级阻塞:
- 任务入队后,按插入顺序依次出队
- 只要出队速度 ≥ 入队速度,任务很快被处理
- 即使短暂积压(集中入队超过 1000/gt),队列也会在后续 gt 清空
因此,平坦分布下抑制器效率低—— 任务按 FIFO 快速流转,无法形成持久积压。
2.4.3 对比表格
2.5 START vs PLAYER 的根本差异
START 和 PLAYER 都是不休眠票,但它们驱动任务队列的方式不同。
2.5.1 START(出生点加载)
MinecraftServer.prepareStartRegion() 在 line 513 添加 START 票,radius = 11;按 level = 33 - radius,出生点中心为 level = 22,周围区块形成 22 到 33 的天然梯度。
关键在于 runTasksTillTickEnd() 阻塞式清空(line 790-793):它先执行一次 runTasks(),再持续 runTasks(() -> !this.shouldKeepTicking()),直到所有 pending 任务清空。
START 的梯度分布(level = 22~33)产生强大的优先级阻塞效应:
- 低 level(22-25)的任务优先被处理
- 高 level(31-33)的任务被持续 "卡" 在队列后方
- 即使
runTasksTillTickEnd()阻塞清空,梯度结构在每次 runTasks () 中都保持有效 - 每次出队时,低 level 任务仍然优先,高 level 任务仍然延迟
runTasksTillTickEnd() 并不会 "抵消" 梯度优势 —— 它只是加快了整体清空速度,但不改变出队的优先级顺序。高 level 区块的光照任务仍然被低 level 阻塞,只是最终都会被清完(因为是阻塞等待)。
结果:START 对光照抑制器效率最强—— 天然梯度 + 持续的优先级阻塞,高 level 区块的光照任务始终排在队列后方。
2.5.2 PLAYER/PORTAL(玩家加载/传送门加载)
PLAYER/PORTAL 加载走正常 tick。ServerLightingProvider.tick() 在有 pending 光照任务时调用一次 runTasks(),每次最多取 1000 个任务。
PLAYER/PORTAL 的抑制效率取决于视距内的 level 分布:
情况 A:梯度分布(如传送门加载器创建多层 level)
- 低 level 任务阻塞高 level 任务
- 目标区块(高 level)的光照任务持久积压
- 抑制效率较高
情况 B:平坦分布(视距内全部 level = 31 或 level = 33)
- 任务按 FIFO 快速流转
- 只有短暂积压(集中入队超过 1000/gt 时)
- 抑制效率较低
但无论哪种分布,PLAYER 的出队速度受 1000/gt 限制,远低于 START 的阻塞清空。因此:
PLAYER(梯度)对光照抑制器有较好效果,但效率低于 START 的天然梯度;PLAYER(平坦)效率很低。
2.5.3 对比表格
2.6 同 level 内插入顺序的影响
同 level 的任务没有第二个数值优先级。LevelPrioritizedQueue.add() 在 line 49-51 用 computeIfAbsent(pos, ...) 把同一区块的任务追加进该区块的 list;poll() 在 line 93-94 通过 firstLongKey() 和 removeFirst() 取出该 level 内最早插入的区块。
因此,即使机器都在同一个 level,先被加载、先向该 level 桶插入任务的区块也会先被处理。机器的空间排列会改变区块首次加载顺序和任务插入顺序,从而改变同 level 内哪些机器先被消化、哪些机器更容易留在队列后方积压。
2.7 原有积压对效率的微小影响
ServerLightingProvider.enqueue() 不是只等下一个 tick 才处理。当包装后的光照任务真正运行并追加到 pendingTasks 后,line 134-136 会在队列大小达到 1000 时立即调用 runTasks():
如果 LQ 已有积压,新入队任务更快把 pendingTasks.size() 推到 1000,批处理会在当前任务执行链中立刻触发,而不是等待下一次 ServerLightingProvider.tick()。这个机制提高的是 "触发批处理的及时性",不是 1000/gt 的长期出队上限;它带来的效果是微小减少等待下一个 tick 的延迟。
2.8 光照抑制导致的区块加载抑制
光照抑制器的目标不仅是 "让光照任务排队",更重要的是通过光照阶段阻塞区块推进,进而抑制整个区块加载链。
2.8.1 光照阶段是加载的瓶颈
区块生成管线的 12 步中,LIGHT 是倒数第二步:
... → FEATURES → INITIALIZE_LIGHT → LIGHT → FULL
- LIGHT 阶段:计算并传播光照,完成后区块达到
ChunkStatus.LIGHT - FULL 阶段:ProtoChunk 转为 WorldChunk,完成后区块达到
ChunkStatus.FULL
只有达到 FULL,区块才能:
- 被
ChunkHolder的accessibleFuture识别为 "可访问"(→ 07 章) - 参与后续的温度升级(tickingFuture、entityTickingFuture)
- 被其他区块作为 "已完成的依赖"(如 3×3 FULL、5×5 FULL)
如果光照任务积压,区块卡在 LIGHT 阶段,就无法推进到 FULL。
2.8.2 连锁阻塞效应
区块加载存在依赖传播:
- BLOCK_TICKING(计划刻、随机刻)需要 3×3 FULL(→ 07/08 章)
- ENTITY_TICKING(实体 AI、刷怪)需要 5×5 FULL(→ 07/10 章)
光照抑制器通过让目标区块的光照任务积压,实现:
- 直接效果:目标区块卡在 LIGHT,无法达到 FULL
- 间接效果:依赖目标区块的周围区块也无法升温
- 中心区块卡在 LIGHT → 周围 3×3 无法全部 FULL → 无法进入 BLOCK_TICKING
- 中心区块卡在 LIGHT → 周围 5×5 无法全部 FULL → 无法进入 ENTITY_TICKING
结果:整个区域的加载被 "冻结" 在光照阶段,即使 ticket 存在、ChunkStatus 管线已执行到 FEATURES,区块仍然无法进入运行态。
2.8.3 与 ChunkHolder 温度管线的关系
ChunkHolder.tick() 的升温判定(→ 07 章)检查的是 ChunkStatus 是否达到 FULL,而不是 "光照任务是否完成"。光照抑制器通过让光照任务排队,间接阻止 ChunkStatus 推进到 FULL,从而阻止 ChunkHolder 升温。
光照任务积压(LQ > 0)
↓
ServerLightingProvider 无法及时完成 LIGHT 阶段
↓
ChunkStatus 卡在 LIGHT,无法推进到 FULL
↓
ChunkHolder.accessibleFuture 无法 complete
↓
tickingFuture、entityTickingFuture 无法开始升温
↓
区块无法进入 BLOCK_TICKING 或 ENTITY_TICKING
↓
计划刻、随机刻、实体刻全部停止
这就是为什么光照抑制器被称为 "区块加载抑制器"—— 它抑制的不是 ticket 或 level,而是 ChunkStatus 推进和 ChunkHolder 升温。
2.8.4 抑制范围的几何特性
由于 3×3 和 5×5 的依赖关系,光照抑制器的影响范围不是单个区块:
- 抑制 1 个区块的光照 → 影响其周围 3×3 = 9 个区块的 BLOCK_TICKING
- 抑制 1 个区块的光照 → 影响其周围 5×5 = 25 个区块的 ENTITY_TICKING
如果在多个位置部署光照抑制器(如沿着边界每隔几个区块放置一台),抑制范围会叠加,形成大面积的 "冻结区域"。
3 参考
- Discovering Minecraft(CC0 协议)
net.minecraft.server.world.ServerLightingProvidernet.minecraft.server.world.ChunkTaskPrioritySystemnet.minecraft.util.thread.LevelPrioritizedQueuenet.minecraft.server.world.ChunkTicketManager
